From a9cb31e75fe269519f6be71ae83627ee21d1bc1f Mon Sep 17 00:00:00 2001 From: Ac_K Date: Fri, 8 Jan 2021 09:14:13 +0100 Subject: gui: Refactoring Part 1 (#1859) * gui: Refactoring Part 1 * Fix ProfileDialog.glade path * Fix Application.Quit assert * Fix TitleUpdateWindow parent * Fix TitleUpdate selected item * Remove extra line in TitleUpdateWindow * Fix empty assign of Enum.TryParse * Add Patrons list in the About Window * update about error messages --- Ryujinx.Common/System/ForceDedicatedGpu.cs | 16 + .../System/WindowsMultimediaTimerResolution.cs | 21 +- Ryujinx/Configuration/DiscordIntegrationModule.cs | 172 -- Ryujinx/Modules/DiscordIntegrationModule.cs | 173 ++ Ryujinx/Modules/Motion/Client.cs | 459 ++++ Ryujinx/Modules/Motion/MotionDevice.cs | 81 + Ryujinx/Modules/Motion/MotionInput.cs | 85 + Ryujinx/Modules/Motion/MotionSensorFilter.cs | 162 ++ Ryujinx/Modules/Motion/Protocol/ControllerData.cs | 51 + Ryujinx/Modules/Motion/Protocol/ControllerInfo.cs | 21 + Ryujinx/Modules/Motion/Protocol/Header.cs | 14 + Ryujinx/Modules/Motion/Protocol/MessageType.cs | 9 + Ryujinx/Modules/Motion/Protocol/SharedResponse.cs | 51 + Ryujinx/Modules/Updater/UpdateDialog.cs | 89 + Ryujinx/Modules/Updater/UpdateDialog.glade | 127 ++ Ryujinx/Modules/Updater/Updater.cs | 360 +++ Ryujinx/Motion/Client.cs | 459 ---- Ryujinx/Motion/MotionDevice.cs | 81 - Ryujinx/Motion/MotionInput.cs | 85 - Ryujinx/Motion/MotionSensorFilter.cs | 166 -- Ryujinx/Motion/Protocol/ControllerData.cs | 50 - Ryujinx/Motion/Protocol/ControllerInfo.cs | 21 - Ryujinx/Motion/Protocol/Header.cs | 14 - Ryujinx/Motion/Protocol/MessageType.cs | 9 - Ryujinx/Motion/Protocol/SharedResponse.cs | 51 - Ryujinx/Program.cs | 80 +- Ryujinx/Ryujinx.csproj | 82 +- Ryujinx/Ui/AboutWindow.cs | 74 - Ryujinx/Ui/AboutWindow.glade | 574 ----- Ryujinx/Ui/App/ApplicationAddedEventArgs.cs | 9 + Ryujinx/Ui/App/ApplicationCountUpdatedEventArgs.cs | 10 + Ryujinx/Ui/App/ApplicationData.cs | 21 + Ryujinx/Ui/App/ApplicationLibrary.cs | 651 ++++++ Ryujinx/Ui/App/ApplicationMetadata.cs | 9 + Ryujinx/Ui/Applet/ErrorAppletDialog.cs | 27 + Ryujinx/Ui/Applet/GtkHostUiHandler.cs | 190 ++ Ryujinx/Ui/Applet/SwkbdAppletDialog.cs | 89 + Ryujinx/Ui/ApplicationAddedEventArgs.cs | 9 - Ryujinx/Ui/ApplicationCountUpdatedEventArgs.cs | 10 - Ryujinx/Ui/ApplicationData.cs | 22 - Ryujinx/Ui/ApplicationLibrary.cs | 650 ------ Ryujinx/Ui/ApplicationMetadata.cs | 9 - Ryujinx/Ui/ControllerWindow.cs | 1034 --------- Ryujinx/Ui/ControllerWindow.glade | 2038 ----------------- Ryujinx/Ui/Diagnostic/GuideDialog.cs | 36 - Ryujinx/Ui/Diagnostic/SetupValidator.cs | 118 - Ryujinx/Ui/Diagnostic/UserError.cs | 39 - Ryujinx/Ui/Diagnostic/UserErrorDialog.cs | 137 -- Ryujinx/Ui/DlcWindow.cs | 265 --- Ryujinx/Ui/DlcWindow.glade | 202 -- Ryujinx/Ui/ErrorAppletDialog.cs | 32 - Ryujinx/Ui/GLRenderer.cs | 11 +- Ryujinx/Ui/GameTableContextMenu.cs | 750 ------ Ryujinx/Ui/GtkDialog.cs | 65 - Ryujinx/Ui/GtkHostUiHandler.cs | 183 -- Ryujinx/Ui/Helper/OpenHelper.cs | 39 + Ryujinx/Ui/Helper/SetupValidator.cs | 119 + Ryujinx/Ui/Helper/SortHelper.cs | 116 + Ryujinx/Ui/Helper/ThemeHelper.cs | 35 + Ryujinx/Ui/InputDialog.cs | 69 - Ryujinx/Ui/MainWindow.cs | 821 +++---- Ryujinx/Ui/Migration.cs | 189 -- Ryujinx/Ui/ProfileDialog.cs | 58 - Ryujinx/Ui/ProfileDialog.glade | 124 - Ryujinx/Ui/Resources/Controller_JoyConLeft.svg | 105 + Ryujinx/Ui/Resources/Controller_JoyConPair.svg | 218 ++ Ryujinx/Ui/Resources/Controller_JoyConRight.svg | 120 + Ryujinx/Ui/Resources/Controller_ProCon.svg | 149 ++ Ryujinx/Ui/Resources/Icon_NCA.png | Bin 0 -> 13675 bytes Ryujinx/Ui/Resources/Icon_NRO.png | Bin 0 -> 13902 bytes Ryujinx/Ui/Resources/Icon_NSO.png | Bin 0 -> 13948 bytes Ryujinx/Ui/Resources/Icon_NSP.png | Bin 0 -> 13198 bytes Ryujinx/Ui/Resources/Icon_XCI.png | Bin 0 -> 13093 bytes Ryujinx/Ui/Resources/Logo_Discord.png | Bin 0 -> 13609 bytes Ryujinx/Ui/Resources/Logo_GitHub.png | Bin 0 -> 26883 bytes Ryujinx/Ui/Resources/Logo_Patreon.png | Bin 0 -> 15483 bytes Ryujinx/Ui/Resources/Logo_Ryujinx.png | Bin 0 -> 53785 bytes Ryujinx/Ui/Resources/Logo_Twitter.png | Bin 0 -> 16937 bytes Ryujinx/Ui/SaveImporter.cs | 219 -- Ryujinx/Ui/SettingsWindow.cs | 577 ----- Ryujinx/Ui/SettingsWindow.glade | 2411 -------------------- Ryujinx/Ui/StatusUpdatedEventArgs.cs | 2 +- Ryujinx/Ui/TitleUpdateWindow.cs | 208 -- Ryujinx/Ui/TitleUpdateWindow.glade | 214 -- Ryujinx/Ui/UrlHelper.cs | 29 - .../Ui/Widgets/GameTableContextMenu.Designer.cs | 198 ++ Ryujinx/Ui/Widgets/GameTableContextMenu.cs | 579 +++++ Ryujinx/Ui/Widgets/GtkDialog.cs | 82 + Ryujinx/Ui/Widgets/ProfileDialog.cs | 55 + Ryujinx/Ui/Widgets/ProfileDialog.glade | 124 + Ryujinx/Ui/Widgets/UserError.cs | 39 + Ryujinx/Ui/Widgets/UserErrorDialog.cs | 122 + Ryujinx/Ui/Windows/AboutWindow.Designer.cs | 467 ++++ Ryujinx/Ui/Windows/AboutWindow.cs | 73 + Ryujinx/Ui/Windows/ControllerWindow.cs | 1027 +++++++++ Ryujinx/Ui/Windows/ControllerWindow.glade | 2038 +++++++++++++++++ Ryujinx/Ui/Windows/DlcWindow.cs | 253 ++ Ryujinx/Ui/Windows/DlcWindow.glade | 202 ++ Ryujinx/Ui/Windows/SettingsWindow.cs | 575 +++++ Ryujinx/Ui/Windows/SettingsWindow.glade | 2411 ++++++++++++++++++++ Ryujinx/Ui/Windows/TitleUpdateWindow.cs | 202 ++ Ryujinx/Ui/Windows/TitleUpdateWindow.glade | 214 ++ Ryujinx/Ui/assets/DiscordLogo.png | Bin 13609 -> 0 bytes Ryujinx/Ui/assets/GitHubLogo.png | Bin 26883 -> 0 bytes Ryujinx/Ui/assets/Icon.png | Bin 53785 -> 0 bytes Ryujinx/Ui/assets/JoyConLeft.svg | 105 - Ryujinx/Ui/assets/JoyConPair.svg | 218 -- Ryujinx/Ui/assets/JoyConRight.svg | 120 - Ryujinx/Ui/assets/NCAIcon.png | Bin 13675 -> 0 bytes Ryujinx/Ui/assets/NROIcon.png | Bin 13902 -> 0 bytes Ryujinx/Ui/assets/NSOIcon.png | Bin 13948 -> 0 bytes Ryujinx/Ui/assets/NSPIcon.png | Bin 13198 -> 0 bytes Ryujinx/Ui/assets/PatreonLogo.png | Bin 15483 -> 0 bytes Ryujinx/Ui/assets/ProCon.svg | 149 -- Ryujinx/Ui/assets/TwitterLogo.png | Bin 16937 -> 0 bytes Ryujinx/Ui/assets/XCIIcon.png | Bin 13093 -> 0 bytes Ryujinx/Updater/UpdateDialog.cs | 88 - Ryujinx/Updater/UpdateDialog.glade | 127 -- Ryujinx/Updater/Updater.cs | 359 --- 119 files changed, 12666 insertions(+), 13236 deletions(-) create mode 100644 Ryujinx.Common/System/ForceDedicatedGpu.cs delete mode 100644 Ryujinx/Configuration/DiscordIntegrationModule.cs create mode 100644 Ryujinx/Modules/DiscordIntegrationModule.cs create mode 100644 Ryujinx/Modules/Motion/Client.cs create mode 100644 Ryujinx/Modules/Motion/MotionDevice.cs create mode 100644 Ryujinx/Modules/Motion/MotionInput.cs create mode 100644 Ryujinx/Modules/Motion/MotionSensorFilter.cs create mode 100644 Ryujinx/Modules/Motion/Protocol/ControllerData.cs create mode 100644 Ryujinx/Modules/Motion/Protocol/ControllerInfo.cs create mode 100644 Ryujinx/Modules/Motion/Protocol/Header.cs create mode 100644 Ryujinx/Modules/Motion/Protocol/MessageType.cs create mode 100644 Ryujinx/Modules/Motion/Protocol/SharedResponse.cs create mode 100644 Ryujinx/Modules/Updater/UpdateDialog.cs create mode 100644 Ryujinx/Modules/Updater/UpdateDialog.glade create mode 100644 Ryujinx/Modules/Updater/Updater.cs delete mode 100644 Ryujinx/Motion/Client.cs delete mode 100644 Ryujinx/Motion/MotionDevice.cs delete mode 100644 Ryujinx/Motion/MotionInput.cs delete mode 100644 Ryujinx/Motion/MotionSensorFilter.cs delete mode 100644 Ryujinx/Motion/Protocol/ControllerData.cs delete mode 100644 Ryujinx/Motion/Protocol/ControllerInfo.cs delete mode 100644 Ryujinx/Motion/Protocol/Header.cs delete mode 100644 Ryujinx/Motion/Protocol/MessageType.cs delete mode 100644 Ryujinx/Motion/Protocol/SharedResponse.cs delete mode 100644 Ryujinx/Ui/AboutWindow.cs delete mode 100644 Ryujinx/Ui/AboutWindow.glade create mode 100644 Ryujinx/Ui/App/ApplicationAddedEventArgs.cs create mode 100644 Ryujinx/Ui/App/ApplicationCountUpdatedEventArgs.cs create mode 100644 Ryujinx/Ui/App/ApplicationData.cs create mode 100644 Ryujinx/Ui/App/ApplicationLibrary.cs create mode 100644 Ryujinx/Ui/App/ApplicationMetadata.cs create mode 100644 Ryujinx/Ui/Applet/ErrorAppletDialog.cs create mode 100644 Ryujinx/Ui/Applet/GtkHostUiHandler.cs create mode 100644 Ryujinx/Ui/Applet/SwkbdAppletDialog.cs delete mode 100644 Ryujinx/Ui/ApplicationAddedEventArgs.cs delete mode 100644 Ryujinx/Ui/ApplicationCountUpdatedEventArgs.cs delete mode 100644 Ryujinx/Ui/ApplicationData.cs delete mode 100644 Ryujinx/Ui/ApplicationLibrary.cs delete mode 100644 Ryujinx/Ui/ApplicationMetadata.cs delete mode 100644 Ryujinx/Ui/ControllerWindow.cs delete mode 100644 Ryujinx/Ui/ControllerWindow.glade delete mode 100644 Ryujinx/Ui/Diagnostic/GuideDialog.cs delete mode 100644 Ryujinx/Ui/Diagnostic/SetupValidator.cs delete mode 100644 Ryujinx/Ui/Diagnostic/UserError.cs delete mode 100644 Ryujinx/Ui/Diagnostic/UserErrorDialog.cs delete mode 100644 Ryujinx/Ui/DlcWindow.cs delete mode 100644 Ryujinx/Ui/DlcWindow.glade delete mode 100644 Ryujinx/Ui/ErrorAppletDialog.cs delete mode 100644 Ryujinx/Ui/GameTableContextMenu.cs delete mode 100644 Ryujinx/Ui/GtkDialog.cs delete mode 100644 Ryujinx/Ui/GtkHostUiHandler.cs create mode 100644 Ryujinx/Ui/Helper/OpenHelper.cs create mode 100644 Ryujinx/Ui/Helper/SetupValidator.cs create mode 100644 Ryujinx/Ui/Helper/SortHelper.cs create mode 100644 Ryujinx/Ui/Helper/ThemeHelper.cs delete mode 100644 Ryujinx/Ui/InputDialog.cs delete mode 100644 Ryujinx/Ui/Migration.cs delete mode 100644 Ryujinx/Ui/ProfileDialog.cs delete mode 100644 Ryujinx/Ui/ProfileDialog.glade create mode 100644 Ryujinx/Ui/Resources/Controller_JoyConLeft.svg create mode 100644 Ryujinx/Ui/Resources/Controller_JoyConPair.svg create mode 100644 Ryujinx/Ui/Resources/Controller_JoyConRight.svg create mode 100644 Ryujinx/Ui/Resources/Controller_ProCon.svg create mode 100644 Ryujinx/Ui/Resources/Icon_NCA.png create mode 100644 Ryujinx/Ui/Resources/Icon_NRO.png create mode 100644 Ryujinx/Ui/Resources/Icon_NSO.png create mode 100644 Ryujinx/Ui/Resources/Icon_NSP.png create mode 100644 Ryujinx/Ui/Resources/Icon_XCI.png create mode 100644 Ryujinx/Ui/Resources/Logo_Discord.png create mode 100644 Ryujinx/Ui/Resources/Logo_GitHub.png create mode 100644 Ryujinx/Ui/Resources/Logo_Patreon.png create mode 100644 Ryujinx/Ui/Resources/Logo_Ryujinx.png create mode 100644 Ryujinx/Ui/Resources/Logo_Twitter.png delete mode 100644 Ryujinx/Ui/SaveImporter.cs delete mode 100644 Ryujinx/Ui/SettingsWindow.cs delete mode 100644 Ryujinx/Ui/SettingsWindow.glade delete mode 100644 Ryujinx/Ui/TitleUpdateWindow.cs delete mode 100644 Ryujinx/Ui/TitleUpdateWindow.glade delete mode 100644 Ryujinx/Ui/UrlHelper.cs create mode 100644 Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs create mode 100644 Ryujinx/Ui/Widgets/GameTableContextMenu.cs create mode 100644 Ryujinx/Ui/Widgets/GtkDialog.cs create mode 100644 Ryujinx/Ui/Widgets/ProfileDialog.cs create mode 100644 Ryujinx/Ui/Widgets/ProfileDialog.glade create mode 100644 Ryujinx/Ui/Widgets/UserError.cs create mode 100644 Ryujinx/Ui/Widgets/UserErrorDialog.cs create mode 100644 Ryujinx/Ui/Windows/AboutWindow.Designer.cs create mode 100644 Ryujinx/Ui/Windows/AboutWindow.cs create mode 100644 Ryujinx/Ui/Windows/ControllerWindow.cs create mode 100644 Ryujinx/Ui/Windows/ControllerWindow.glade create mode 100644 Ryujinx/Ui/Windows/DlcWindow.cs create mode 100644 Ryujinx/Ui/Windows/DlcWindow.glade create mode 100644 Ryujinx/Ui/Windows/SettingsWindow.cs create mode 100644 Ryujinx/Ui/Windows/SettingsWindow.glade create mode 100644 Ryujinx/Ui/Windows/TitleUpdateWindow.cs create mode 100644 Ryujinx/Ui/Windows/TitleUpdateWindow.glade delete mode 100644 Ryujinx/Ui/assets/DiscordLogo.png delete mode 100644 Ryujinx/Ui/assets/GitHubLogo.png delete mode 100644 Ryujinx/Ui/assets/Icon.png delete mode 100644 Ryujinx/Ui/assets/JoyConLeft.svg delete mode 100644 Ryujinx/Ui/assets/JoyConPair.svg delete mode 100644 Ryujinx/Ui/assets/JoyConRight.svg delete mode 100644 Ryujinx/Ui/assets/NCAIcon.png delete mode 100644 Ryujinx/Ui/assets/NROIcon.png delete mode 100644 Ryujinx/Ui/assets/NSOIcon.png delete mode 100644 Ryujinx/Ui/assets/NSPIcon.png delete mode 100644 Ryujinx/Ui/assets/PatreonLogo.png delete mode 100644 Ryujinx/Ui/assets/ProCon.svg delete mode 100644 Ryujinx/Ui/assets/TwitterLogo.png delete mode 100644 Ryujinx/Ui/assets/XCIIcon.png delete mode 100644 Ryujinx/Updater/UpdateDialog.cs delete mode 100644 Ryujinx/Updater/UpdateDialog.glade delete mode 100644 Ryujinx/Updater/Updater.cs diff --git a/Ryujinx.Common/System/ForceDedicatedGpu.cs b/Ryujinx.Common/System/ForceDedicatedGpu.cs new file mode 100644 index 00000000..60272f1a --- /dev/null +++ b/Ryujinx.Common/System/ForceDedicatedGpu.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Common.System +{ + public static class ForceDedicatedGpu + { + public static void Nvidia() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // NOTE: If the DLL exists, we can load it to force the usage of the dedicated Nvidia Gpu. + NativeLibrary.TryLoad("nvapi64.dll", out _); + } + } + } +} \ No newline at end of file diff --git a/Ryujinx.Common/System/WindowsMultimediaTimerResolution.cs b/Ryujinx.Common/System/WindowsMultimediaTimerResolution.cs index fccda247..69ce145b 100644 --- a/Ryujinx.Common/System/WindowsMultimediaTimerResolution.cs +++ b/Ryujinx.Common/System/WindowsMultimediaTimerResolution.cs @@ -17,14 +17,14 @@ namespace Ryujinx.Common.System public uint wPeriodMax; }; - [DllImport("winmm.dll", SetLastError = true)] - private static extern uint timeGetDevCaps(ref TimeCaps timeCaps, uint sizeTimeCaps); + [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps", SetLastError = true)] + private static extern uint TimeGetDevCaps(ref TimeCaps timeCaps, uint sizeTimeCaps); - [DllImport("winmm.dll")] - private static extern uint timeBeginPeriod(uint uMilliseconds); + [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")] + private static extern uint TimeBeginPeriod(uint uMilliseconds); - [DllImport("winmm.dll")] - private static extern uint timeEndPeriod(uint uMilliseconds); + [DllImport("winmm.dll", EntryPoint = "timeEndPeriod")] + private static extern uint TimeEndPeriod(uint uMilliseconds); private uint _targetResolutionInMilliseconds; private bool _isActive; @@ -45,7 +45,7 @@ namespace Ryujinx.Common.System { TimeCaps timeCaps = default; - uint result = timeGetDevCaps(ref timeCaps, (uint)Unsafe.SizeOf()); + uint result = TimeGetDevCaps(ref timeCaps, (uint)Unsafe.SizeOf()); if (result != 0) { @@ -66,7 +66,7 @@ namespace Ryujinx.Common.System private void Activate() { - uint result = timeBeginPeriod(_targetResolutionInMilliseconds); + uint result = TimeBeginPeriod(_targetResolutionInMilliseconds); if (result != 0) { @@ -82,7 +82,7 @@ namespace Ryujinx.Common.System { if (_isActive) { - uint result = timeEndPeriod(_targetResolutionInMilliseconds); + uint result = TimeEndPeriod(_targetResolutionInMilliseconds); if (result != 0) { @@ -98,6 +98,7 @@ namespace Ryujinx.Common.System public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) @@ -108,4 +109,4 @@ namespace Ryujinx.Common.System } } } -} +} \ No newline at end of file diff --git a/Ryujinx/Configuration/DiscordIntegrationModule.cs b/Ryujinx/Configuration/DiscordIntegrationModule.cs deleted file mode 100644 index 378e9c3f..00000000 --- a/Ryujinx/Configuration/DiscordIntegrationModule.cs +++ /dev/null @@ -1,172 +0,0 @@ -using DiscordRPC; -using Ryujinx.Common; -using System; -using System.Linq; - -namespace Ryujinx.Configuration -{ - static class DiscordIntegrationModule - { - private static DiscordRpcClient _discordClient; - - private static string LargeDescription = "Ryujinx is a Nintendo Switch emulator."; - - public static RichPresence DiscordPresence { get; private set; } - - public static void Initialize() - { - DiscordPresence = new RichPresence - { - Assets = new Assets - { - LargeImageKey = "ryujinx", - LargeImageText = LargeDescription - }, - Details = "Main Menu", - State = "Idling", - Timestamps = new Timestamps(DateTime.UtcNow) - }; - - ConfigurationState.Instance.EnableDiscordIntegration.Event += Update; - } - - private static void Update(object sender, ReactiveEventArgs e) - { - if (e.OldValue != e.NewValue) - { - // If the integration was active, disable it and unload everything - if (e.OldValue) - { - _discordClient?.Dispose(); - - _discordClient = null; - } - - // If we need to activate it and the client isn't active, initialize it - if (e.NewValue && _discordClient == null) - { - _discordClient = new DiscordRpcClient("568815339807309834"); - - _discordClient.Initialize(); - _discordClient.SetPresence(DiscordPresence); - } - } - } - - public static void SwitchToPlayingState(string titleId, string titleName) - { - if (SupportedTitles.Contains(titleId)) - { - DiscordPresence.Assets.LargeImageKey = titleId; - } - - string state = titleId; - - if (state == null) - { - state = "Ryujinx"; - } - else - { - state = state.ToUpper(); - } - - string details = "Idling"; - - if (titleName != null) - { - details = $"Playing {titleName}"; - } - - DiscordPresence.Details = details; - DiscordPresence.State = state; - DiscordPresence.Assets.LargeImageText = titleName; - DiscordPresence.Assets.SmallImageKey = "ryujinx"; - DiscordPresence.Assets.SmallImageText = LargeDescription; - DiscordPresence.Timestamps = new Timestamps(DateTime.UtcNow); - - _discordClient?.SetPresence(DiscordPresence); - } - - public static void SwitchToMainMenu() - { - DiscordPresence.Details = "Main Menu"; - DiscordPresence.State = "Idling"; - DiscordPresence.Assets.LargeImageKey = "ryujinx"; - DiscordPresence.Assets.LargeImageText = LargeDescription; - DiscordPresence.Assets.SmallImageKey = null; - DiscordPresence.Assets.SmallImageText = null; - DiscordPresence.Timestamps = new Timestamps(DateTime.UtcNow); - - _discordClient?.SetPresence(DiscordPresence); - } - - public static void Exit() - { - _discordClient?.Dispose(); - } - - private static readonly string[] SupportedTitles = - { - "0100000000010000", // Super Mario Odyssey™ - "01000b900d8b0000", // Cadence of Hyrule – Crypt of the NecroDancer Featuring The Legend of Zelda - "01000d200ac0c000", // Bud Spencer & Terence Hill - Slaps And Beans - "01000d700be88000", // My Girlfriend is a Mermaid!? - "01000dc007e90000", // Sparkle Unleashed - "01000e2003fa0000", // MIGHTY GUNVOLT BURST - "0100225000fee000", // Blaster Master Zero - "010028d0045ce000", // Sparkle 2 - "01002b30028f6000", // Celeste - "01002fc00c6d0000", // Witch Thief - "010034e005c9c000", // Code of Princess EX - "010036b0034e4000", // Super Mario Party™ - "01003d200baa2000", // Pokémon Mystery Dungeon™: Rescue Team DX - "01004f8006a78000", // Super Meat Boy - "010051f00ac5e000", // SEGA AGES Sonic The Hedgehog - "010055d009f78000", // Fire Emblem™: Three Houses - "010056e00853a000", // A Hat in Time - "0100574009f9e000", // 嘘つき姫と盲目王子 - "01005d700e742000", // DOOM 64 - "0100628004bce000", // Nights of Azure 2: Bride of the New Moon - "0100633007d48000", // Hollow Knight - "010065500b218000", // メモリーズオフ -Innocent Fille- - "010068f00aa78000", // FINAL FANTASY XV POCKET EDITION HD - "01006bb00c6f0000", // The Legend of Zelda™: Link’s Awakening - "01006f8002326000", // Animal Crossing™: New Horizons - "01006a800016e000", // Super Smash Bros.™ Ultimate - "010072800cbe8000", // PC Building Simulator - "01007300020fa000", // ASTRAL CHAIN - "01007330027ee000", // Ultra Street Fighter® II: The Final Challengers - "0100749009844000", // 20XX - "01007a4008486000", // Enchanting Mahjong Match - "01007ef00011e000", // The Legend of Zelda™: Breath of the Wild - "010080b00ad66000", // Undertale - "010082400bcc6000", // Untitled Goose Game - "01008db008c2c000", // Pokémon™ Shield - "010094e00b52e000", // Capcom Beat 'Em Up Bundle - "01009aa000faa000", // Sonic Mania - "01009b90006dc000", // Super Mario Maker™ 2 - "01009cc00c97c000", // DEAD OR ALIVE Xtreme 3 Scarlet 基本無料版 - "0100ea80032ea000", // New Super Mario Bros.™ U Deluxe - "0100a4200a284000", // LUMINES REMASTERED - "0100a5c00d162000", // Cuphead - "0100abf008968000", // Pokémon™ Sword - "0100ae000aebc000", // Angels of Death - "0100b3f000be2000", // Pokkén Tournament™ DX - "0100bc2004ff4000", // Owlboy - "0100cf3007578000", // Atari Flashback Classics - "0100d5d00c6be000", // Our World Is Ended. - "0100d6b00cd88000", // YUMENIKKI -DREAM DIARY- - "0100d870045b6000", // Nintendo Entertainment System™ - Nintendo Switch Online - "0100e0c00adac000", // SENRAN KAGURA Reflexions - "0100e46006708000", // Terraria - "0100e7200b272000", // Lanota - "0100e9f00b882000", // null - "0100eab00605c000", // Poly Bridge - "0100efd00a4fa000", // Shantae and the Pirate's Curse - "0100f6a00a684000", // ひぐらしのなく頃に奉 - "0100f9f00c696000", // Crash™ Team Racing Nitro-Fueled - "051337133769a000", // RGB-Seizure - }; - } -} diff --git a/Ryujinx/Modules/DiscordIntegrationModule.cs b/Ryujinx/Modules/DiscordIntegrationModule.cs new file mode 100644 index 00000000..3a91cf53 --- /dev/null +++ b/Ryujinx/Modules/DiscordIntegrationModule.cs @@ -0,0 +1,173 @@ +using DiscordRPC; +using Ryujinx.Common; +using Ryujinx.Configuration; +using System; +using System.Linq; + +namespace Ryujinx.Modules +{ + static class DiscordIntegrationModule + { + private static DiscordRpcClient _discordClient; + + private const string LargeDescription = "Ryujinx is a Nintendo Switch emulator."; + + public static RichPresence DiscordPresence { get; private set; } + + public static void Initialize() + { + DiscordPresence = new RichPresence + { + Assets = new Assets + { + LargeImageKey = "ryujinx", + LargeImageText = LargeDescription + }, + Details = "Main Menu", + State = "Idling", + Timestamps = new Timestamps(DateTime.UtcNow) + }; + + ConfigurationState.Instance.EnableDiscordIntegration.Event += Update; + } + + private static void Update(object sender, ReactiveEventArgs e) + { + if (e.OldValue != e.NewValue) + { + // If the integration was active, disable it and unload everything + if (e.OldValue) + { + _discordClient?.Dispose(); + + _discordClient = null; + } + + // If we need to activate it and the client isn't active, initialize it + if (e.NewValue && _discordClient == null) + { + _discordClient = new DiscordRpcClient("568815339807309834"); + + _discordClient.Initialize(); + _discordClient.SetPresence(DiscordPresence); + } + } + } + + public static void SwitchToPlayingState(string titleId, string titleName) + { + if (SupportedTitles.Contains(titleId)) + { + DiscordPresence.Assets.LargeImageKey = titleId; + } + + string state = titleId; + + if (state == null) + { + state = "Ryujinx"; + } + else + { + state = state.ToUpper(); + } + + string details = "Idling"; + + if (titleName != null) + { + details = $"Playing {titleName}"; + } + + DiscordPresence.Details = details; + DiscordPresence.State = state; + DiscordPresence.Assets.LargeImageText = titleName; + DiscordPresence.Assets.SmallImageKey = "ryujinx"; + DiscordPresence.Assets.SmallImageText = LargeDescription; + DiscordPresence.Timestamps = new Timestamps(DateTime.UtcNow); + + _discordClient?.SetPresence(DiscordPresence); + } + + public static void SwitchToMainMenu() + { + DiscordPresence.Details = "Main Menu"; + DiscordPresence.State = "Idling"; + DiscordPresence.Assets.LargeImageKey = "ryujinx"; + DiscordPresence.Assets.LargeImageText = LargeDescription; + DiscordPresence.Assets.SmallImageKey = null; + DiscordPresence.Assets.SmallImageText = null; + DiscordPresence.Timestamps = new Timestamps(DateTime.UtcNow); + + _discordClient?.SetPresence(DiscordPresence); + } + + public static void Exit() + { + _discordClient?.Dispose(); + } + + private static readonly string[] SupportedTitles = + { + "0100000000010000", // Super Mario Odyssey™ + "01000b900d8b0000", // Cadence of Hyrule – Crypt of the NecroDancer Featuring The Legend of Zelda + "01000d200ac0c000", // Bud Spencer & Terence Hill - Slaps And Beans + "01000d700be88000", // My Girlfriend is a Mermaid!? + "01000dc007e90000", // Sparkle Unleashed + "01000e2003fa0000", // MIGHTY GUNVOLT BURST + "0100225000fee000", // Blaster Master Zero + "010028d0045ce000", // Sparkle 2 + "01002b30028f6000", // Celeste + "01002fc00c6d0000", // Witch Thief + "010034e005c9c000", // Code of Princess EX + "010036b0034e4000", // Super Mario Party™ + "01003d200baa2000", // Pokémon Mystery Dungeon™: Rescue Team DX + "01004f8006a78000", // Super Meat Boy + "010051f00ac5e000", // SEGA AGES Sonic The Hedgehog + "010055d009f78000", // Fire Emblem™: Three Houses + "010056e00853a000", // A Hat in Time + "0100574009f9e000", // 嘘つき姫と盲目王子 + "01005d700e742000", // DOOM 64 + "0100628004bce000", // Nights of Azure 2: Bride of the New Moon + "0100633007d48000", // Hollow Knight + "010065500b218000", // メモリーズオフ -Innocent Fille- + "010068f00aa78000", // FINAL FANTASY XV POCKET EDITION HD + "01006bb00c6f0000", // The Legend of Zelda™: Link’s Awakening + "01006f8002326000", // Animal Crossing™: New Horizons + "01006a800016e000", // Super Smash Bros.™ Ultimate + "010072800cbe8000", // PC Building Simulator + "01007300020fa000", // ASTRAL CHAIN + "01007330027ee000", // Ultra Street Fighter® II: The Final Challengers + "0100749009844000", // 20XX + "01007a4008486000", // Enchanting Mahjong Match + "01007ef00011e000", // The Legend of Zelda™: Breath of the Wild + "010080b00ad66000", // Undertale + "010082400bcc6000", // Untitled Goose Game + "01008db008c2c000", // Pokémon™ Shield + "010094e00b52e000", // Capcom Beat 'Em Up Bundle + "01009aa000faa000", // Sonic Mania + "01009b90006dc000", // Super Mario Maker™ 2 + "01009cc00c97c000", // DEAD OR ALIVE Xtreme 3 Scarlet 基本無料版 + "0100ea80032ea000", // New Super Mario Bros.™ U Deluxe + "0100a4200a284000", // LUMINES REMASTERED + "0100a5c00d162000", // Cuphead + "0100abf008968000", // Pokémon™ Sword + "0100ae000aebc000", // Angels of Death + "0100b3f000be2000", // Pokkén Tournament™ DX + "0100bc2004ff4000", // Owlboy + "0100cf3007578000", // Atari Flashback Classics + "0100d5d00c6be000", // Our World Is Ended. + "0100d6b00cd88000", // YUMENIKKI -DREAM DIARY- + "0100d870045b6000", // Nintendo Entertainment System™ - Nintendo Switch Online + "0100e0c00adac000", // SENRAN KAGURA Reflexions + "0100e46006708000", // Terraria + "0100e7200b272000", // Lanota + "0100e9f00b882000", // null + "0100eab00605c000", // Poly Bridge + "0100efd00a4fa000", // Shantae and the Pirate's Curse + "0100f6a00a684000", // ひぐらしのなく頃に奉 + "0100f9f00c696000", // Crash™ Team Racing Nitro-Fueled + "051337133769a000", // RGB-Seizure + }; + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Motion/Client.cs b/Ryujinx/Modules/Motion/Client.cs new file mode 100644 index 00000000..3f18fb7f --- /dev/null +++ b/Ryujinx/Modules/Motion/Client.cs @@ -0,0 +1,459 @@ +using Force.Crc32; +using Ryujinx.Common; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Logging; +using Ryujinx.Configuration; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Numerics; +using System.Threading.Tasks; + +namespace Ryujinx.Modules.Motion +{ + public class Client : IDisposable + { + public const uint Magic = 0x43555344; // DSUC + public const ushort Version = 1001; + + private bool _active; + + private readonly Dictionary _hosts; + private readonly Dictionary> _motionData; + private readonly Dictionary _clients; + + private readonly bool[] _clientErrorStatus = new bool[Enum.GetValues(typeof(PlayerIndex)).Length]; + private readonly long[] _clientRetryTimer = new long[Enum.GetValues(typeof(PlayerIndex)).Length]; + + public Client() + { + _hosts = new Dictionary(); + _motionData = new Dictionary>(); + _clients = new Dictionary(); + + CloseClients(); + } + + public void CloseClients() + { + _active = false; + + lock (_clients) + { + foreach (var client in _clients) + { + try + { + client.Value?.Dispose(); + } + catch (SocketException socketException) + { + Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to dispose motion client. Error: {socketException.ErrorCode}"); + } + } + + _hosts.Clear(); + _clients.Clear(); + _motionData.Clear(); + } + } + + public void RegisterClient(int player, string host, int port) + { + if (_clients.ContainsKey(player) || !CanConnect(player)) + { + return; + } + + lock (_clients) + { + if (_clients.ContainsKey(player) || !CanConnect(player)) + { + return; + } + + UdpClient client = null; + + try + { + IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(host), port); + + client = new UdpClient(host, port); + + _clients.Add(player, client); + _hosts.Add(player, endPoint); + + _active = true; + + Task.Run(() => + { + ReceiveLoop(player); + }); + } + catch (FormatException formatException) + { + if (!_clientErrorStatus[player]) + { + Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error: {formatException.Message}"); + + _clientErrorStatus[player] = true; + } + } + catch (SocketException socketException) + { + if (!_clientErrorStatus[player]) + { + Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error: {socketException.ErrorCode}"); + + _clientErrorStatus[player] = true; + } + + RemoveClient(player); + + client?.Dispose(); + + SetRetryTimer(player); + } + catch (Exception exception) + { + Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to register motion client. Error: {exception.Message}"); + + _clientErrorStatus[player] = true; + + RemoveClient(player); + + client?.Dispose(); + + SetRetryTimer(player); + } + } + } + + public bool TryGetData(int player, int slot, out MotionInput input) + { + lock (_motionData) + { + if (_motionData.ContainsKey(player)) + { + if (_motionData[player].TryGetValue(slot, out input)) + { + return true; + } + } + } + + input = null; + + return false; + } + + private void RemoveClient(int clientId) + { + _clients?.Remove(clientId); + + _hosts?.Remove(clientId); + } + + private void Send(byte[] data, int clientId) + { + if (_clients.TryGetValue(clientId, out UdpClient _client)) + { + if (_client != null && _client.Client != null && _client.Client.Connected) + { + try + { + _client?.Send(data, data.Length); + } + catch (SocketException socketException) + { + if (!_clientErrorStatus[clientId]) + { + Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to send data request to motion source at {_client.Client.RemoteEndPoint}. Error: {socketException.ErrorCode}"); + } + + _clientErrorStatus[clientId] = true; + + RemoveClient(clientId); + + _client?.Dispose(); + + SetRetryTimer(clientId); + } + catch (ObjectDisposedException) + { + _clientErrorStatus[clientId] = true; + + RemoveClient(clientId); + + _client?.Dispose(); + + SetRetryTimer(clientId); + } + } + } + } + + private byte[] Receive(int clientId, int timeout = 0) + { + if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint) && _clients.TryGetValue(clientId, out UdpClient _client)) + { + if (_client != null && _client.Client != null && _client.Client.Connected) + { + _client.Client.ReceiveTimeout = timeout; + + var result = _client?.Receive(ref endPoint); + + if (result.Length > 0) + { + _clientErrorStatus[clientId] = false; + } + + return result; + } + } + + throw new Exception($"Client {clientId} is not registered."); + } + + private void SetRetryTimer(int clientId) + { + var elapsedMs = PerformanceCounter.ElapsedMilliseconds; + + _clientRetryTimer[clientId] = elapsedMs; + } + + private void ResetRetryTimer(int clientId) + { + _clientRetryTimer[clientId] = 0; + } + + private bool CanConnect(int clientId) + { + return _clientRetryTimer[clientId] == 0 || PerformanceCounter.ElapsedMilliseconds - 5000 > _clientRetryTimer[clientId]; + } + + public void ReceiveLoop(int clientId) + { + if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint) && _clients.TryGetValue(clientId, out UdpClient _client)) + { + if (_client != null && _client.Client != null && _client.Client.Connected) + { + try + { + while (_active) + { + byte[] data = Receive(clientId); + + if (data.Length == 0) + { + continue; + } + + Task.Run(() => HandleResponse(data, clientId)); + } + } + catch (SocketException socketException) + { + if (!_clientErrorStatus[clientId]) + { + Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to receive data from motion source at {endPoint}. Error: {socketException.ErrorCode}"); + } + + _clientErrorStatus[clientId] = true; + + RemoveClient(clientId); + + _client?.Dispose(); + + SetRetryTimer(clientId); + } + catch (ObjectDisposedException) + { + _clientErrorStatus[clientId] = true; + + RemoveClient(clientId); + + _client?.Dispose(); + + SetRetryTimer(clientId); + } + } + } + } + + public void HandleResponse(byte[] data, int clientId) + { + ResetRetryTimer(clientId); + + MessageType type = (MessageType)BitConverter.ToUInt32(data.AsSpan().Slice(16, 4)); + + data = data.AsSpan()[16..].ToArray(); + + using MemoryStream stream = new MemoryStream(data); + using BinaryReader reader = new BinaryReader(stream); + + switch (type) + { + case MessageType.Protocol: + break; + case MessageType.Info: + ControllerInfoResponse contollerInfo = reader.ReadStruct(); + break; + case MessageType.Data: + ControllerDataResponse inputData = reader.ReadStruct(); + + Vector3 accelerometer = new Vector3() + { + X = -inputData.AccelerometerX, + Y = inputData.AccelerometerZ, + Z = -inputData.AccelerometerY + }; + + Vector3 gyroscrope = new Vector3() + { + X = inputData.GyroscopePitch, + Y = inputData.GyroscopeRoll, + Z = -inputData.GyroscopeYaw + }; + + ulong timestamp = inputData.MotionTimestamp; + + InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == (PlayerIndex)clientId); + + lock (_motionData) + { + int slot = inputData.Shared.Slot; + + if (_motionData.ContainsKey(clientId)) + { + if (_motionData[clientId].ContainsKey(slot)) + { + MotionInput previousData = _motionData[clientId][slot]; + + previousData.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone); + } + else + { + MotionInput input = new MotionInput(); + + input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone); + + _motionData[clientId].Add(slot, input); + } + } + else + { + MotionInput input = new MotionInput(); + + input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone); + + _motionData.Add(clientId, new Dictionary() { { slot, input } }); + } + } + break; + } + } + + public void RequestInfo(int clientId, int slot) + { + if (!_active) + { + return; + } + + Header header = GenerateHeader(clientId); + + using (MemoryStream stream = new MemoryStream()) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.WriteStruct(header); + + ControllerInfoRequest request = new ControllerInfoRequest() + { + Type = MessageType.Info, + PortsCount = 4 + }; + + request.PortIndices[0] = (byte)slot; + + writer.WriteStruct(request); + + header.Length = (ushort)(stream.Length - 16); + + writer.Seek(6, SeekOrigin.Begin); + writer.Write(header.Length); + + header.Crc32 = Crc32Algorithm.Compute(stream.ToArray()); + + writer.Seek(8, SeekOrigin.Begin); + writer.Write(header.Crc32); + + byte[] data = stream.ToArray(); + + Send(data, clientId); + } + } + + public unsafe void RequestData(int clientId, int slot) + { + if (!_active) + { + return; + } + + Header header = GenerateHeader(clientId); + + using (MemoryStream stream = new MemoryStream()) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.WriteStruct(header); + + ControllerDataRequest request = new ControllerDataRequest() + { + Type = MessageType.Data, + Slot = (byte)slot, + SubscriberType = SubscriberType.Slot + }; + + writer.WriteStruct(request); + + header.Length = (ushort)(stream.Length - 16); + + writer.Seek(6, SeekOrigin.Begin); + writer.Write(header.Length); + + header.Crc32 = Crc32Algorithm.Compute(stream.ToArray()); + + writer.Seek(8, SeekOrigin.Begin); + writer.Write(header.Crc32); + + byte[] data = stream.ToArray(); + + Send(data, clientId); + } + } + + private Header GenerateHeader(int clientId) + { + Header header = new Header() + { + Id = (uint)clientId, + MagicString = Magic, + Version = Version, + Length = 0, + Crc32 = 0 + }; + + return header; + } + + public void Dispose() + { + _active = false; + + CloseClients(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Motion/MotionDevice.cs b/Ryujinx/Modules/Motion/MotionDevice.cs new file mode 100644 index 00000000..4fb9b422 --- /dev/null +++ b/Ryujinx/Modules/Motion/MotionDevice.cs @@ -0,0 +1,81 @@ +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Configuration; +using System; +using System.Numerics; + +namespace Ryujinx.Modules.Motion +{ + public class MotionDevice + { + public Vector3 Gyroscope { get; private set; } + public Vector3 Accelerometer { get; private set; } + public Vector3 Rotation { get; private set; } + public float[] Orientation { get; private set; } + + private readonly Client _motionSource; + + public MotionDevice(Client motionSource) + { + _motionSource = motionSource; + } + + public void RegisterController(PlayerIndex player) + { + InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == player); + + if (config != null && config.EnableMotion) + { + string host = config.DsuServerHost; + int port = config.DsuServerPort; + + _motionSource.RegisterClient((int)player, host, port); + _motionSource.RequestData((int)player, config.Slot); + + if (config.ControllerType == ControllerType.JoyconPair && !config.MirrorInput) + { + _motionSource.RequestData((int)player, config.AltSlot); + } + } + } + + public void Poll(InputConfig config, int slot) + { + Orientation = new float[9]; + + if (!config.EnableMotion || !_motionSource.TryGetData((int)config.PlayerIndex, slot, out MotionInput input)) + { + Accelerometer = new Vector3(); + Gyroscope = new Vector3(); + + return; + } + + Gyroscope = Truncate(input.Gyroscrope * 0.0027f, 3); + Accelerometer = Truncate(input.Accelerometer, 3); + Rotation = Truncate(input.Rotation * 0.0027f, 3); + + Matrix4x4 orientation = input.GetOrientation(); + + Orientation[0] = Math.Clamp(orientation.M11, -1f, 1f); + Orientation[1] = Math.Clamp(orientation.M12, -1f, 1f); + Orientation[2] = Math.Clamp(orientation.M13, -1f, 1f); + Orientation[3] = Math.Clamp(orientation.M21, -1f, 1f); + Orientation[4] = Math.Clamp(orientation.M22, -1f, 1f); + Orientation[5] = Math.Clamp(orientation.M23, -1f, 1f); + Orientation[6] = Math.Clamp(orientation.M31, -1f, 1f); + Orientation[7] = Math.Clamp(orientation.M32, -1f, 1f); + Orientation[8] = Math.Clamp(orientation.M33, -1f, 1f); + } + + private static Vector3 Truncate(Vector3 value, int decimals) + { + float power = MathF.Pow(10, decimals); + + value.X = float.IsNegative(value.X) ? MathF.Ceiling(value.X * power) / power : MathF.Floor(value.X * power) / power; + value.Y = float.IsNegative(value.Y) ? MathF.Ceiling(value.Y * power) / power : MathF.Floor(value.Y * power) / power; + value.Z = float.IsNegative(value.Z) ? MathF.Ceiling(value.Z * power) / power : MathF.Floor(value.Z * power) / power; + + return value; + } + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Motion/MotionInput.cs b/Ryujinx/Modules/Motion/MotionInput.cs new file mode 100644 index 00000000..3e4d9768 --- /dev/null +++ b/Ryujinx/Modules/Motion/MotionInput.cs @@ -0,0 +1,85 @@ +using System; +using System.Numerics; + +namespace Ryujinx.Modules.Motion +{ + public class MotionInput + { + public ulong TimeStamp { get; set; } + public Vector3 Accelerometer { get; set; } + public Vector3 Gyroscrope { get; set; } + public Vector3 Rotation { get; set; } + + private readonly MotionSensorFilter _filter; + private int _calibrationFrame = 0; + + public MotionInput() + { + TimeStamp = 0; + Accelerometer = new Vector3(); + Gyroscrope = new Vector3(); + Rotation = new Vector3(); + + // TODO: RE the correct filter. + _filter = new MotionSensorFilter(0f); + } + + public void Update(Vector3 accel, Vector3 gyro, ulong timestamp, int sensitivity, float deadzone) + { + if (TimeStamp != 0) + { + if (gyro.Length() <= 1f && accel.Length() >= 0.8f && accel.Z >= 0.8f) + { + _calibrationFrame++; + + if (_calibrationFrame >= 90) + { + gyro = Vector3.Zero; + + Rotation = Vector3.Zero; + + _filter.Reset(); + + _calibrationFrame = 0; + } + } + else + { + _calibrationFrame = 0; + } + + Accelerometer = -accel; + + if (gyro.Length() < deadzone) + { + gyro = Vector3.Zero; + } + + gyro *= (sensitivity / 100f); + + Gyroscrope = gyro; + + float deltaTime = MathF.Abs((long)(timestamp - TimeStamp) / 1000000f); + + Vector3 deltaGyro = gyro * deltaTime; + + Rotation += deltaGyro; + + _filter.SamplePeriod = deltaTime; + _filter.Update(accel, DegreeToRad(gyro)); + } + + TimeStamp = timestamp; + } + + public Matrix4x4 GetOrientation() + { + return Matrix4x4.CreateFromQuaternion(_filter.Quaternion); + } + + private static Vector3 DegreeToRad(Vector3 degree) + { + return degree * (MathF.PI / 180); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Motion/MotionSensorFilter.cs b/Ryujinx/Modules/Motion/MotionSensorFilter.cs new file mode 100644 index 00000000..b83e61a8 --- /dev/null +++ b/Ryujinx/Modules/Motion/MotionSensorFilter.cs @@ -0,0 +1,162 @@ +using System.Numerics; + +namespace Ryujinx.Modules.Motion +{ + // MahonyAHRS class. Madgwick's implementation of Mayhony's AHRS algorithm. + // See: https://x-io.co.uk/open-source-imu-and-ahrs-algorithms/ + // Based on: https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MahonyAHRS.cs + public class MotionSensorFilter + { + /// + /// Sample rate coefficient. + /// + public const float SampleRateCoefficient = 0.45f; + + /// + /// Gets or sets the sample period. + /// + public float SamplePeriod { get; set; } + + /// + /// Gets or sets the algorithm proportional gain. + /// + public float Kp { get; set; } + + /// + /// Gets or sets the algorithm integral gain. + /// + public float Ki { get; set; } + + /// + /// Gets the Quaternion output. + /// + public Quaternion Quaternion { get; private set; } + + /// + /// Integral error. + /// + private Vector3 _intergralError; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Sample period. + /// + public MotionSensorFilter(float samplePeriod) : this(samplePeriod, 1f, 0f) { } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Sample period. + /// + /// + /// Algorithm proportional gain. + /// + public MotionSensorFilter(float samplePeriod, float kp) : this(samplePeriod, kp, 0f) { } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Sample period. + /// + /// + /// Algorithm proportional gain. + /// + /// + /// Algorithm integral gain. + /// + public MotionSensorFilter(float samplePeriod, float kp, float ki) + { + SamplePeriod = samplePeriod; + Kp = kp; + Ki = ki; + + Reset(); + + _intergralError = new Vector3(); + } + + /// + /// Algorithm IMU update method. Requires only gyroscope and accelerometer data. + /// + /// + /// Accelerometer measurement in any calibrated units. + /// + /// + /// Gyroscope measurement in radians. + /// + public void Update(Vector3 accel, Vector3 gyro) + { + // Normalise accelerometer measurement. + float norm = 1f / accel.Length(); + + if (!float.IsFinite(norm)) + { + return; + } + + accel *= norm; + + float q2 = Quaternion.X; + float q3 = Quaternion.Y; + float q4 = Quaternion.Z; + float q1 = Quaternion.W; + + // Estimated direction of gravity. + Vector3 gravity = new Vector3() + { + X = 2f * (q2 * q4 - q1 * q3), + Y = 2f * (q1 * q2 + q3 * q4), + Z = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4 + }; + + // Error is cross product between estimated direction and measured direction of gravity. + Vector3 error = new Vector3() + { + X = accel.Y * gravity.Z - accel.Z * gravity.Y, + Y = accel.Z * gravity.X - accel.X * gravity.Z, + Z = accel.X * gravity.Y - accel.Y * gravity.X + }; + + if (Ki > 0f) + { + _intergralError += error; // Accumulate integral error. + } + else + { + _intergralError = Vector3.Zero; // Prevent integral wind up. + } + + // Apply feedback terms. + gyro += (Kp * error) + (Ki * _intergralError); + + // Integrate rate of change of quaternion. + Vector3 delta = new Vector3(q2, q3, q4); + + q1 += (-q2 * gyro.X - q3 * gyro.Y - q4 * gyro.Z) * (SampleRateCoefficient * SamplePeriod); + q2 += (q1 * gyro.X + delta.Y * gyro.Z - delta.Z * gyro.Y) * (SampleRateCoefficient * SamplePeriod); + q3 += (q1 * gyro.Y - delta.X * gyro.Z + delta.Z * gyro.X) * (SampleRateCoefficient * SamplePeriod); + q4 += (q1 * gyro.Z + delta.X * gyro.Y - delta.Y * gyro.X) * (SampleRateCoefficient * SamplePeriod); + + // Normalise quaternion. + Quaternion quaternion = new Quaternion(q2, q3, q4, q1); + + norm = 1f / quaternion.Length(); + + if (!float.IsFinite(norm)) + { + return; + } + + Quaternion = quaternion * norm; + } + + public void Reset() + { + Quaternion = Quaternion.Identity; + } + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Motion/Protocol/ControllerData.cs b/Ryujinx/Modules/Motion/Protocol/ControllerData.cs new file mode 100644 index 00000000..30972b86 --- /dev/null +++ b/Ryujinx/Modules/Motion/Protocol/ControllerData.cs @@ -0,0 +1,51 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Modules.Motion +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ControllerDataRequest + { + public MessageType Type; + public SubscriberType SubscriberType; + public byte Slot; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + public byte[] MacAddress; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ControllerDataResponse + { + public SharedResponse Shared; + public byte Connected; + public uint PacketId; + public byte ExtraButtons; + public byte MainButtons; + public ushort PSExtraInput; + public ushort LeftStickXY; + public ushort RightStickXY; + public uint DPadAnalog; + public ulong MainButtonsAnalog; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + public byte[] Touch1; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + public byte[] Touch2; + + public ulong MotionTimestamp; + public float AccelerometerX; + public float AccelerometerY; + public float AccelerometerZ; + public float GyroscopePitch; + public float GyroscopeYaw; + public float GyroscopeRoll; + } + + enum SubscriberType : byte + { + All, + Slot, + Mac + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Motion/Protocol/ControllerInfo.cs b/Ryujinx/Modules/Motion/Protocol/ControllerInfo.cs new file mode 100644 index 00000000..63e2db5b --- /dev/null +++ b/Ryujinx/Modules/Motion/Protocol/ControllerInfo.cs @@ -0,0 +1,21 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Modules.Motion +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ControllerInfoResponse + { + public SharedResponse Shared; + private byte _zero; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ControllerInfoRequest + { + public MessageType Type; + public int PortsCount; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] PortIndices; + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Motion/Protocol/Header.cs b/Ryujinx/Modules/Motion/Protocol/Header.cs new file mode 100644 index 00000000..39d8b531 --- /dev/null +++ b/Ryujinx/Modules/Motion/Protocol/Header.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Modules.Motion +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct Header + { + public uint MagicString; + public ushort Version; + public ushort Length; + public uint Crc32; + public uint Id; + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Motion/Protocol/MessageType.cs b/Ryujinx/Modules/Motion/Protocol/MessageType.cs new file mode 100644 index 00000000..3cbb6579 --- /dev/null +++ b/Ryujinx/Modules/Motion/Protocol/MessageType.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Modules.Motion +{ + public enum MessageType : uint + { + Protocol = 0x100000, + Info, + Data + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Motion/Protocol/SharedResponse.cs b/Ryujinx/Modules/Motion/Protocol/SharedResponse.cs new file mode 100644 index 00000000..e5b03722 --- /dev/null +++ b/Ryujinx/Modules/Motion/Protocol/SharedResponse.cs @@ -0,0 +1,51 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.Modules.Motion +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct SharedResponse + { + public MessageType Type; + public byte Slot; + public SlotState State; + public DeviceModelType ModelType; + public ConnectionType ConnectionType; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + public byte[] MacAddress; + public BatteryStatus BatteryStatus; + } + + public enum SlotState : byte + { + Disconnected, + Reserved, + Connected + } + + public enum DeviceModelType : byte + { + None, + PartialGyro, + FullGyro + } + + public enum ConnectionType : byte + { + None, + USB, + Bluetooth + } + + public enum BatteryStatus : byte + { + NA, + Dying, + Low, + Medium, + High, + Full, + Charging, + Charged + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Updater/UpdateDialog.cs b/Ryujinx/Modules/Updater/UpdateDialog.cs new file mode 100644 index 00000000..cb38383a --- /dev/null +++ b/Ryujinx/Modules/Updater/UpdateDialog.cs @@ -0,0 +1,89 @@ +using Gdk; +using Gtk; +using Mono.Unix; +using Ryujinx.Ui; +using System; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Ryujinx.Modules +{ + public class UpdateDialog : Gtk.Window + { +#pragma warning disable CS0649, IDE0044 + [Builder.Object] public Label MainText; + [Builder.Object] public Label SecondaryText; + [Builder.Object] public LevelBar ProgressBar; + [Builder.Object] public Button YesButton; + [Builder.Object] public Button NoButton; +#pragma warning restore CS0649, IDE0044 + + private readonly MainWindow _mainWindow; + private readonly string _buildUrl; + private bool _restartQuery; + + public UpdateDialog(MainWindow mainWindow, Version newVersion, string buildUrl) : this(new Builder("Ryujinx..Modules.Updater.UpdateDialog.glade"), mainWindow, newVersion, buildUrl) { } + + private UpdateDialog(Builder builder, MainWindow mainWindow, Version newVersion, string buildUrl) : base(builder.GetObject("UpdateDialog").Handle) + { + builder.Autoconnect(this); + + _mainWindow = mainWindow; + _buildUrl = buildUrl; + + MainText.Text = "Do you want to update Ryujinx to the latest version?"; + SecondaryText.Text = $"{Program.Version} -> {newVersion}"; + + ProgressBar.Hide(); + + YesButton.Clicked += YesButton_Clicked; + NoButton.Clicked += NoButton_Clicked; + } + + private void YesButton_Clicked(object sender, EventArgs args) + { + if (_restartQuery) + { + string ryuName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Ryujinx.exe" : "Ryujinx"; + string ryuExe = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ryuName); + string ryuArg = string.Join(" ", Environment.GetCommandLineArgs().AsEnumerable().Skip(1).ToArray()); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + UnixFileInfo unixFileInfo = new UnixFileInfo(ryuExe); + unixFileInfo.FileAccessPermissions |= FileAccessPermissions.UserExecute; + } + + Process.Start(ryuExe, ryuArg); + + Environment.Exit(0); + } + else + { + Window.Functions = _mainWindow.Window.Functions = WMFunction.All & WMFunction.Close; + _mainWindow.ExitMenuItem.Sensitive = false; + + YesButton.Hide(); + NoButton.Hide(); + ProgressBar.Show(); + + SecondaryText.Text = ""; + _restartQuery = true; + + _ = Updater.UpdateRyujinx(this, _buildUrl); + } + } + + private void NoButton_Clicked(object sender, EventArgs args) + { + Updater.Running = false; + _mainWindow.Window.Functions = WMFunction.All; + + _mainWindow.ExitMenuItem.Sensitive = true; + _mainWindow.UpdateMenuItem.Sensitive = true; + + Dispose(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Modules/Updater/UpdateDialog.glade b/Ryujinx/Modules/Updater/UpdateDialog.glade new file mode 100644 index 00000000..cc80167e --- /dev/null +++ b/Ryujinx/Modules/Updater/UpdateDialog.glade @@ -0,0 +1,127 @@ + + + + + + False + Ryujinx - Updater + False + center + 400 + 130 + + + + + + True + False + 10 + 10 + 10 + 10 + vertical + + + True + False + vertical + + + True + False + 5 + 5 + + + + + + + False + True + 0 + + + + + True + False + 5 + 5 + + + False + True + 1 + + + + + 20 + True + False + 5 + 5 + 100 + + + False + True + 2 + + + + + True + True + 0 + + + + + True + False + + + Yes + True + True + True + 5 + 5 + 5 + + + True + True + 0 + + + + + No + True + True + True + 5 + 5 + 5 + + + True + True + 1 + + + + + False + True + 1 + + + + + + diff --git a/Ryujinx/Modules/Updater/Updater.cs b/Ryujinx/Modules/Updater/Updater.cs new file mode 100644 index 00000000..35d67adf --- /dev/null +++ b/Ryujinx/Modules/Updater/Updater.cs @@ -0,0 +1,360 @@ +using Gtk; +using ICSharpCode.SharpZipLib.GZip; +using ICSharpCode.SharpZipLib.Tar; +using ICSharpCode.SharpZipLib.Zip; +using Newtonsoft.Json.Linq; +using Ryujinx.Common.Logging; +using Ryujinx.Ui; +using Ryujinx.Ui.Widgets; +using System; +using System.IO; +using System.Net; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Ryujinx.Modules +{ + public static class Updater + { + internal static bool Running; + + private static readonly string HomeDir = AppDomain.CurrentDomain.BaseDirectory; + private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update"); + private static readonly string UpdatePublishDir = Path.Combine(UpdateDir, "publish"); + + private static string _jobId; + private static string _buildVer; + private static string _platformExt; + private static string _buildUrl; + + private const string AppveyorApiUrl = "https://ci.appveyor.com/api"; + + public static async Task BeginParse(MainWindow mainWindow, bool showVersionUpToDate) + { + if (Running) return; + + Running = true; + mainWindow.UpdateMenuItem.Sensitive = false; + + // Detect current platform + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + _platformExt = "osx_x64.zip"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _platformExt = "win_x64.zip"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + _platformExt = "linux_x64.tar.gz"; + } + + Version newVersion; + Version currentVersion; + + try + { + currentVersion = Version.Parse(Program.Version); + } + catch + { + GtkDialog.CreateWarningDialog("Failed to convert the current Ryujinx version.", "Cancelling Update!"); + Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!"); + + return; + } + + // Get latest version number from Appveyor + try + { + using (WebClient jsonClient = new WebClient()) + { + string fetchedJson = await jsonClient.DownloadStringTaskAsync($"{AppveyorApiUrl}/projects/gdkchan/ryujinx/branch/master"); + JObject jsonRoot = JObject.Parse(fetchedJson); + JToken buildToken = jsonRoot["build"]; + + _jobId = (string)buildToken["jobs"][0]["jobId"]; + _buildVer = (string)buildToken["version"]; + _buildUrl = $"{AppveyorApiUrl}/buildjobs/{_jobId}/artifacts/ryujinx-{_buildVer}-{_platformExt}"; + + // If build not done, assume no new update are availaible. + if ((string)buildToken["jobs"][0]["status"] != "success") + { + if (showVersionUpToDate) + { + GtkDialog.CreateUpdaterInfoDialog("You are already using the most updated version of Ryujinx!", ""); + } + + return; + } + } + } + catch (Exception exception) + { + Logger.Error?.Print(LogClass.Application, exception.Message); + GtkDialog.CreateErrorDialog("An error has occurred when trying to get release information from AppVeyor."); + + return; + } + + try + { + newVersion = Version.Parse(_buildVer); + } + catch + { + GtkDialog.CreateWarningDialog("Failed to convert the received Ryujinx version from AppVeyor.", "Cancelling Update!"); + Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from AppVeyor!"); + + return; + } + + if (newVersion <= currentVersion) + { + if (showVersionUpToDate) + { + GtkDialog.CreateUpdaterInfoDialog("You are already using the most updated version of Ryujinx!", ""); + } + + Running = false; + mainWindow.UpdateMenuItem.Sensitive = true; + + return; + } + + // Show a message asking the user if they want to update + UpdateDialog updateDialog = new UpdateDialog(mainWindow, newVersion, _buildUrl); + updateDialog.Show(); + } + + public static async Task UpdateRyujinx(UpdateDialog updateDialog, string downloadUrl) + { + // Empty update dir, although it shouldn't ever have anything inside it + if (Directory.Exists(UpdateDir)) + { + Directory.Delete(UpdateDir, true); + } + + Directory.CreateDirectory(UpdateDir); + + string updateFile = Path.Combine(UpdateDir, "update.bin"); + + // Download the update .zip + updateDialog.MainText.Text = "Downloading Update..."; + updateDialog.ProgressBar.Value = 0; + updateDialog.ProgressBar.MaxValue = 100; + + using (WebClient client = new WebClient()) + { + client.DownloadProgressChanged += (_, args) => + { + updateDialog.ProgressBar.Value = args.ProgressPercentage; + }; + + await client.DownloadFileTaskAsync(downloadUrl, updateFile); + } + + // Extract Update + updateDialog.MainText.Text = "Extracting Update..."; + updateDialog.ProgressBar.Value = 0; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + using (Stream inStream = File.OpenRead(updateFile)) + using (Stream gzipStream = new GZipInputStream(inStream)) + using (TarInputStream tarStream = new TarInputStream(gzipStream, Encoding.ASCII)) + { + updateDialog.ProgressBar.MaxValue = inStream.Length; + + await Task.Run(() => + { + TarEntry tarEntry; + while ((tarEntry = tarStream.GetNextEntry()) != null) + { + if (tarEntry.IsDirectory) continue; + + string outPath = Path.Combine(UpdateDir, tarEntry.Name); + + Directory.CreateDirectory(Path.GetDirectoryName(outPath)); + + using (FileStream outStream = File.OpenWrite(outPath)) + { + tarStream.CopyEntryContents(outStream); + } + + File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc)); + + TarEntry entry = tarEntry; + + Application.Invoke(delegate + { + updateDialog.ProgressBar.Value += entry.Size; + }); + } + }); + + updateDialog.ProgressBar.Value = inStream.Length; + } + } + else + { + using (Stream inStream = File.OpenRead(updateFile)) + using (ZipFile zipFile = new ZipFile(inStream)) + { + updateDialog.ProgressBar.MaxValue = zipFile.Count; + + await Task.Run(() => + { + foreach (ZipEntry zipEntry in zipFile) + { + if (zipEntry.IsDirectory) continue; + + string outPath = Path.Combine(UpdateDir, zipEntry.Name); + + Directory.CreateDirectory(Path.GetDirectoryName(outPath)); + + using (Stream zipStream = zipFile.GetInputStream(zipEntry)) + using (FileStream outStream = File.OpenWrite(outPath)) + { + zipStream.CopyTo(outStream); + } + + File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc)); + + Application.Invoke(delegate + { + updateDialog.ProgressBar.Value++; + }); + } + }); + } + } + + // Delete downloaded zip + File.Delete(updateFile); + + string[] allFiles = Directory.GetFiles(HomeDir, "*", SearchOption.AllDirectories); + + updateDialog.MainText.Text = "Renaming Old Files..."; + updateDialog.ProgressBar.Value = 0; + updateDialog.ProgressBar.MaxValue = allFiles.Length; + + // Replace old files + await Task.Run(() => + { + foreach (string file in allFiles) + { + if (!Path.GetExtension(file).Equals(".log")) + { + try + { + File.Move(file, file + ".ryuold"); + + Application.Invoke(delegate + { + updateDialog.ProgressBar.Value++; + }); + } + catch + { + Logger.Warning?.Print(LogClass.Application, "Updater wasn't able to rename file: " + file); + } + } + } + + Application.Invoke(delegate + { + updateDialog.MainText.Text = "Adding New Files..."; + updateDialog.ProgressBar.Value = 0; + updateDialog.ProgressBar.MaxValue = Directory.GetFiles(UpdatePublishDir, "*", SearchOption.AllDirectories).Length; + }); + + MoveAllFilesOver(UpdatePublishDir, HomeDir, updateDialog); + }); + + Directory.Delete(UpdateDir, true); + + updateDialog.MainText.Text = "Update Complete!"; + updateDialog.SecondaryText.Text = "Do you want to restart Ryujinx now?"; + updateDialog.Modal = true; + + updateDialog.ProgressBar.Hide(); + updateDialog.YesButton.Show(); + updateDialog.NoButton.Show(); + } + + public static bool CanUpdate(bool showWarnings) + { + if (RuntimeInformation.OSArchitecture != Architecture.X64) + { + if (showWarnings) + { + GtkDialog.CreateWarningDialog("You are not running a supported system architecture!", "(Only x64 systems are supported!)"); + } + + return false; + } + + if (!NetworkInterface.GetIsNetworkAvailable()) + { + if (showWarnings) + { + GtkDialog.CreateWarningDialog("You are not connected to the Internet!", "Please verify that you have a working Internet connection!"); + } + + return false; + } + + if (Program.Version.Contains("dirty")) + { + if (showWarnings) + { + GtkDialog.CreateWarningDialog("You Cannot update a Dirty build of Ryujinx!", "Please download Ryujinx at https://ryujinx.org/ if you are looking for a supported version."); + } + + return false; + } + + return true; + } + + private static void MoveAllFilesOver(string root, string dest, UpdateDialog dialog) + { + foreach (string directory in Directory.GetDirectories(root)) + { + string dirName = Path.GetFileName(directory); + + if (!Directory.Exists(Path.Combine(dest, dirName))) + { + Directory.CreateDirectory(Path.Combine(dest, dirName)); + } + + MoveAllFilesOver(directory, Path.Combine(dest, dirName), dialog); + } + + foreach (string file in Directory.GetFiles(root)) + { + File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true); + + Application.Invoke(delegate + { + dialog.ProgressBar.Value++; + }); + } + } + + public static void CleanupUpdate() + { + foreach (string file in Directory.GetFiles(HomeDir, "*", SearchOption.AllDirectories)) + { + if (Path.GetExtension(file).EndsWith(".ryuold")) + { + File.Delete(file); + } + } + } + } +} diff --git a/Ryujinx/Motion/Client.cs b/Ryujinx/Motion/Client.cs deleted file mode 100644 index 8c5ad20d..00000000 --- a/Ryujinx/Motion/Client.cs +++ /dev/null @@ -1,459 +0,0 @@ -using Force.Crc32; -using Ryujinx.Common; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Logging; -using Ryujinx.Configuration; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Numerics; -using System.Threading.Tasks; - -namespace Ryujinx.Motion -{ - public class Client : IDisposable - { - public const uint Magic = 0x43555344; // DSUC - public const ushort Version = 1001; - - private bool _active; - - private readonly Dictionary _hosts; - private readonly Dictionary> _motionData; - private readonly Dictionary _clients; - - private readonly bool[] _clientErrorStatus = new bool[Enum.GetValues(typeof(PlayerIndex)).Length]; - private readonly long[] _clientRetryTimer = new long[Enum.GetValues(typeof(PlayerIndex)).Length]; - - public Client() - { - _hosts = new Dictionary(); - _motionData = new Dictionary>(); - _clients = new Dictionary(); - - CloseClients(); - } - - public void CloseClients() - { - _active = false; - - lock (_clients) - { - foreach (var client in _clients) - { - try - { - client.Value?.Dispose(); - } - catch (SocketException socketException) - { - Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to dispose motion client. Error: {socketException.ErrorCode}"); - } - } - - _hosts.Clear(); - _clients.Clear(); - _motionData.Clear(); - } - } - - public void RegisterClient(int player, string host, int port) - { - if (_clients.ContainsKey(player) || !CanConnect(player)) - { - return; - } - - lock (_clients) - { - if (_clients.ContainsKey(player) || !CanConnect(player)) - { - return; - } - - UdpClient client = null; - - try - { - IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(host), port); - - client = new UdpClient(host, port); - - _clients.Add(player, client); - _hosts.Add(player, endPoint); - - _active = true; - - Task.Run(() => - { - ReceiveLoop(player); - }); - } - catch (FormatException formatException) - { - if (!_clientErrorStatus[player]) - { - Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error: {formatException.Message}"); - - _clientErrorStatus[player] = true; - } - } - catch (SocketException socketException) - { - if (!_clientErrorStatus[player]) - { - Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error: {socketException.ErrorCode}"); - - _clientErrorStatus[player] = true; - } - - RemoveClient(player); - - client?.Dispose(); - - SetRetryTimer(player); - } - catch (Exception exception) - { - Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to register motion client. Error: {exception.Message}"); - - _clientErrorStatus[player] = true; - - RemoveClient(player); - - client?.Dispose(); - - SetRetryTimer(player); - } - } - } - - public bool TryGetData(int player, int slot, out MotionInput input) - { - lock (_motionData) - { - if (_motionData.ContainsKey(player)) - { - if (_motionData[player].TryGetValue(slot, out input)) - { - return true; - } - } - } - - input = null; - - return false; - } - - private void RemoveClient(int clientId) - { - _clients?.Remove(clientId); - - _hosts?.Remove(clientId); - } - - private void Send(byte[] data, int clientId) - { - if (_clients.TryGetValue(clientId, out UdpClient _client)) - { - if (_client != null && _client.Client != null && _client.Client.Connected) - { - try - { - _client?.Send(data, data.Length); - } - catch (SocketException socketException) - { - if (!_clientErrorStatus[clientId]) - { - Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to send data request to motion source at {_client.Client.RemoteEndPoint}. Error: {socketException.ErrorCode}"); - } - - _clientErrorStatus[clientId] = true; - - RemoveClient(clientId); - - _client?.Dispose(); - - SetRetryTimer(clientId); - } - catch (ObjectDisposedException) - { - _clientErrorStatus[clientId] = true; - - RemoveClient(clientId); - - _client?.Dispose(); - - SetRetryTimer(clientId); - } - } - } - } - - private byte[] Receive(int clientId, int timeout = 0) - { - if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint) && _clients.TryGetValue(clientId, out UdpClient _client)) - { - if (_client != null && _client.Client != null && _client.Client.Connected) - { - _client.Client.ReceiveTimeout = timeout; - - var result = _client?.Receive(ref endPoint); - - if (result.Length > 0) - { - _clientErrorStatus[clientId] = false; - } - - return result; - } - } - - throw new Exception($"Client {clientId} is not registered."); - } - - private void SetRetryTimer(int clientId) - { - var elapsedMs = PerformanceCounter.ElapsedMilliseconds; - - _clientRetryTimer[clientId] = elapsedMs; - } - - private void ResetRetryTimer(int clientId) - { - _clientRetryTimer[clientId] = 0; - } - - private bool CanConnect(int clientId) - { - return _clientRetryTimer[clientId] == 0 || PerformanceCounter.ElapsedMilliseconds - 5000 > _clientRetryTimer[clientId]; - } - - public void ReceiveLoop(int clientId) - { - if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint) && _clients.TryGetValue(clientId, out UdpClient _client)) - { - if (_client != null && _client.Client != null && _client.Client.Connected) - { - try - { - while (_active) - { - byte[] data = Receive(clientId); - - if (data.Length == 0) - { - continue; - } - - Task.Run(() => HandleResponse(data, clientId)); - } - } - catch (SocketException socketException) - { - if (!_clientErrorStatus[clientId]) - { - Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to receive data from motion source at {endPoint}. Error: {socketException.ErrorCode}"); - } - - _clientErrorStatus[clientId] = true; - - RemoveClient(clientId); - - _client?.Dispose(); - - SetRetryTimer(clientId); - } - catch (ObjectDisposedException) - { - _clientErrorStatus[clientId] = true; - - RemoveClient(clientId); - - _client?.Dispose(); - - SetRetryTimer(clientId); - } - } - } - } - - public void HandleResponse(byte[] data, int clientId) - { - ResetRetryTimer(clientId); - - MessageType type = (MessageType)BitConverter.ToUInt32(data.AsSpan().Slice(16, 4)); - - data = data.AsSpan()[16..].ToArray(); - - using MemoryStream stream = new MemoryStream(data); - using BinaryReader reader = new BinaryReader(stream); - - switch (type) - { - case MessageType.Protocol: - break; - case MessageType.Info: - ControllerInfoResponse contollerInfo = reader.ReadStruct(); - break; - case MessageType.Data: - ControllerDataResponse inputData = reader.ReadStruct(); - - Vector3 accelerometer = new Vector3() - { - X = -inputData.AccelerometerX, - Y = inputData.AccelerometerZ, - Z = -inputData.AccelerometerY - }; - - Vector3 gyroscrope = new Vector3() - { - X = inputData.GyroscopePitch, - Y = inputData.GyroscopeRoll, - Z = -inputData.GyroscopeYaw - }; - - ulong timestamp = inputData.MotionTimestamp; - - InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == (PlayerIndex)clientId); - - lock (_motionData) - { - int slot = inputData.Shared.Slot; - - if (_motionData.ContainsKey(clientId)) - { - if (_motionData[clientId].ContainsKey(slot)) - { - MotionInput previousData = _motionData[clientId][slot]; - - previousData.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone); - } - else - { - MotionInput input = new MotionInput(); - - input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone); - - _motionData[clientId].Add(slot, input); - } - } - else - { - MotionInput input = new MotionInput(); - - input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone); - - _motionData.Add(clientId, new Dictionary() { { slot, input } }); - } - } - break; - } - } - - public void RequestInfo(int clientId, int slot) - { - if (!_active) - { - return; - } - - Header header = GenerateHeader(clientId); - - using (MemoryStream stream = new MemoryStream()) - using (BinaryWriter writer = new BinaryWriter(stream)) - { - writer.WriteStruct(header); - - ControllerInfoRequest request = new ControllerInfoRequest() - { - Type = MessageType.Info, - PortsCount = 4 - }; - - request.PortIndices[0] = (byte)slot; - - writer.WriteStruct(request); - - header.Length = (ushort)(stream.Length - 16); - - writer.Seek(6, SeekOrigin.Begin); - writer.Write(header.Length); - - header.Crc32 = Crc32Algorithm.Compute(stream.ToArray()); - - writer.Seek(8, SeekOrigin.Begin); - writer.Write(header.Crc32); - - byte[] data = stream.ToArray(); - - Send(data, clientId); - } - } - - public unsafe void RequestData(int clientId, int slot) - { - if (!_active) - { - return; - } - - Header header = GenerateHeader(clientId); - - using (MemoryStream stream = new MemoryStream()) - using (BinaryWriter writer = new BinaryWriter(stream)) - { - writer.WriteStruct(header); - - ControllerDataRequest request = new ControllerDataRequest() - { - Type = MessageType.Data, - Slot = (byte)slot, - SubscriberType = SubscriberType.Slot - }; - - writer.WriteStruct(request); - - header.Length = (ushort)(stream.Length - 16); - - writer.Seek(6, SeekOrigin.Begin); - writer.Write(header.Length); - - header.Crc32 = Crc32Algorithm.Compute(stream.ToArray()); - - writer.Seek(8, SeekOrigin.Begin); - writer.Write(header.Crc32); - - byte[] data = stream.ToArray(); - - Send(data, clientId); - } - } - - private Header GenerateHeader(int clientId) - { - Header header = new Header() - { - ID = (uint)clientId, - MagicString = Magic, - Version = Version, - Length = 0, - Crc32 = 0 - }; - - return header; - } - - public void Dispose() - { - _active = false; - - CloseClients(); - } - } -} diff --git a/Ryujinx/Motion/MotionDevice.cs b/Ryujinx/Motion/MotionDevice.cs deleted file mode 100644 index 2e2050bc..00000000 --- a/Ryujinx/Motion/MotionDevice.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Configuration; -using System; -using System.Numerics; - -namespace Ryujinx.Motion -{ - public class MotionDevice - { - public Vector3 Gyroscope { get; private set; } - public Vector3 Accelerometer { get; private set; } - public Vector3 Rotation { get; private set; } - public float[] Orientation { get; private set; } - - private Client _motionSource; - - public MotionDevice(Client motionSource) - { - _motionSource = motionSource; - } - - public void RegisterController(PlayerIndex player) - { - InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == player); - - if (config != null && config.EnableMotion) - { - string host = config.DsuServerHost; - int port = config.DsuServerPort; - - _motionSource.RegisterClient((int)player, host, port); - _motionSource.RequestData((int)player, config.Slot); - - if (config.ControllerType == ControllerType.JoyconPair && !config.MirrorInput) - { - _motionSource.RequestData((int)player, config.AltSlot); - } - } - } - - public void Poll(InputConfig config, int slot) - { - Orientation = new float[9]; - - if (!config.EnableMotion || !_motionSource.TryGetData((int)config.PlayerIndex, slot, out MotionInput input)) - { - Accelerometer = new Vector3(); - Gyroscope = new Vector3(); - - return; - } - - Gyroscope = Truncate(input.Gyroscrope * 0.0027f, 3); - Accelerometer = Truncate(input.Accelerometer, 3); - Rotation = Truncate(input.Rotation * 0.0027f, 3); - - Matrix4x4 orientation = input.GetOrientation(); - - Orientation[0] = Math.Clamp(orientation.M11, -1f, 1f); - Orientation[1] = Math.Clamp(orientation.M12, -1f, 1f); - Orientation[2] = Math.Clamp(orientation.M13, -1f, 1f); - Orientation[3] = Math.Clamp(orientation.M21, -1f, 1f); - Orientation[4] = Math.Clamp(orientation.M22, -1f, 1f); - Orientation[5] = Math.Clamp(orientation.M23, -1f, 1f); - Orientation[6] = Math.Clamp(orientation.M31, -1f, 1f); - Orientation[7] = Math.Clamp(orientation.M32, -1f, 1f); - Orientation[8] = Math.Clamp(orientation.M33, -1f, 1f); - } - - private static Vector3 Truncate(Vector3 value, int decimals) - { - float power = MathF.Pow(10, decimals); - - value.X = float.IsNegative(value.X) ? MathF.Ceiling(value.X * power) / power : MathF.Floor(value.X * power) / power; - value.Y = float.IsNegative(value.Y) ? MathF.Ceiling(value.Y * power) / power : MathF.Floor(value.Y * power) / power; - value.Z = float.IsNegative(value.Z) ? MathF.Ceiling(value.Z * power) / power : MathF.Floor(value.Z * power) / power; - - return value; - } - } -} diff --git a/Ryujinx/Motion/MotionInput.cs b/Ryujinx/Motion/MotionInput.cs deleted file mode 100644 index f767d8cc..00000000 --- a/Ryujinx/Motion/MotionInput.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Numerics; - -namespace Ryujinx.Motion -{ - public class MotionInput - { - public ulong TimeStamp { get; set; } - public Vector3 Accelerometer { get; set; } - public Vector3 Gyroscrope { get; set; } - public Vector3 Rotation { get; set; } - - private readonly MotionSensorFilter _filter; - private int _calibrationFrame = 0; - - public MotionInput() - { - TimeStamp = 0; - Accelerometer = new Vector3(); - Gyroscrope = new Vector3(); - Rotation = new Vector3(); - - // TODO: RE the correct filter. - _filter = new MotionSensorFilter(0f); - } - - public void Update(Vector3 accel, Vector3 gyro, ulong timestamp, int sensitivity, float deadzone) - { - if (TimeStamp != 0) - { - if (gyro.Length() <= 1f && accel.Length() >= 0.8f && accel.Z >= 0.8f) - { - _calibrationFrame++; - - if (_calibrationFrame >= 90) - { - gyro = Vector3.Zero; - - Rotation = Vector3.Zero; - - _filter.Reset(); - - _calibrationFrame = 0; - } - } - else - { - _calibrationFrame = 0; - } - - Accelerometer = -accel; - - if (gyro.Length() < deadzone) - { - gyro = Vector3.Zero; - } - - gyro *= (sensitivity / 100f); - - Gyroscrope = gyro; - - float deltaTime = MathF.Abs((long)(timestamp - TimeStamp) / 1000000f); - - Vector3 deltaGyro = gyro * deltaTime; - - Rotation += deltaGyro; - - _filter.SamplePeriod = deltaTime; - _filter.Update(accel, DegreeToRad(gyro)); - } - - TimeStamp = timestamp; - } - - public Matrix4x4 GetOrientation() - { - return Matrix4x4.CreateFromQuaternion(_filter.Quaternion); - } - - private static Vector3 DegreeToRad(Vector3 degree) - { - return degree * (MathF.PI / 180); - } - } -} diff --git a/Ryujinx/Motion/MotionSensorFilter.cs b/Ryujinx/Motion/MotionSensorFilter.cs deleted file mode 100644 index 5173a191..00000000 --- a/Ryujinx/Motion/MotionSensorFilter.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.Numerics; - -namespace Ryujinx.Motion -{ - // MahonyAHRS class. Madgwick's implementation of Mayhony's AHRS algorithm. - // See: https://x-io.co.uk/open-source-imu-and-ahrs-algorithms/ - // Based on: https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MahonyAHRS.cs - public class MotionSensorFilter - { - /// - /// Sample rate coefficient. - /// - public const float SampleRateCoefficient = 0.45f; - - /// - /// Gets or sets the sample period. - /// - public float SamplePeriod { get; set; } - - /// - /// Gets or sets the algorithm proportional gain. - /// - public float Kp { get; set; } - - /// - /// Gets or sets the algorithm integral gain. - /// - public float Ki { get; set; } - - /// - /// Gets the Quaternion output. - /// - public Quaternion Quaternion { get; private set; } - - /// - /// Integral error. - /// - private Vector3 _intergralError; - - /// - /// Initializes a new instance of the class. - /// - /// - /// Sample period. - /// - public MotionSensorFilter(float samplePeriod) : this(samplePeriod, 1f, 0f) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// Sample period. - /// - /// - /// Algorithm proportional gain. - /// - public MotionSensorFilter(float samplePeriod, float kp) : this(samplePeriod, kp, 0f) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// Sample period. - /// - /// - /// Algorithm proportional gain. - /// - /// - /// Algorithm integral gain. - /// - public MotionSensorFilter(float samplePeriod, float kp, float ki) - { - SamplePeriod = samplePeriod; - Kp = kp; - Ki = ki; - - Reset(); - - _intergralError = new Vector3(); - } - - /// - /// Algorithm IMU update method. Requires only gyroscope and accelerometer data. - /// - /// - /// Accelerometer measurement in any calibrated units. - /// - /// - /// Gyroscope measurement in radians. - /// - public void Update(Vector3 accel, Vector3 gyro) - { - // Normalise accelerometer measurement. - float norm = 1f / accel.Length(); - - if (!float.IsFinite(norm)) - { - return; - } - - accel *= norm; - - float q2 = Quaternion.X; - float q3 = Quaternion.Y; - float q4 = Quaternion.Z; - float q1 = Quaternion.W; - - // Estimated direction of gravity. - Vector3 gravity = new Vector3() - { - X = 2f * (q2 * q4 - q1 * q3), - Y = 2f * (q1 * q2 + q3 * q4), - Z = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4 - }; - - // Error is cross product between estimated direction and measured direction of gravity. - Vector3 error = new Vector3() - { - X = accel.Y * gravity.Z - accel.Z * gravity.Y, - Y = accel.Z * gravity.X - accel.X * gravity.Z, - Z = accel.X * gravity.Y - accel.Y * gravity.X - }; - - if (Ki > 0f) - { - _intergralError += error; // Accumulate integral error. - } - else - { - _intergralError = Vector3.Zero; // Prevent integral wind up. - } - - // Apply feedback terms. - gyro += (Kp * error) + (Ki * _intergralError); - - // Integrate rate of change of quaternion. - Vector3 delta = new Vector3(q2, q3, q4); - - q1 += (-q2 * gyro.X - q3 * gyro.Y - q4 * gyro.Z) * (SampleRateCoefficient * SamplePeriod); - q2 += (q1 * gyro.X + delta.Y * gyro.Z - delta.Z * gyro.Y) * (SampleRateCoefficient * SamplePeriod); - q3 += (q1 * gyro.Y - delta.X * gyro.Z + delta.Z * gyro.X) * (SampleRateCoefficient * SamplePeriod); - q4 += (q1 * gyro.Z + delta.X * gyro.Y - delta.Y * gyro.X) * (SampleRateCoefficient * SamplePeriod); - - // Normalise quaternion. - Quaternion quaternion = new Quaternion(q2, q3, q4, q1); - - norm = 1f / quaternion.Length(); - - if (!float.IsFinite(norm)) - { - return; - } - - Quaternion = quaternion * norm; - } - - public void Reset() - { - Quaternion = Quaternion.Identity; - } - } -} diff --git a/Ryujinx/Motion/Protocol/ControllerData.cs b/Ryujinx/Motion/Protocol/ControllerData.cs deleted file mode 100644 index 4b4919a1..00000000 --- a/Ryujinx/Motion/Protocol/ControllerData.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Ryujinx.Motion -{ - [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct ControllerDataRequest - { - public MessageType Type; - public SubscriberType SubscriberType; - public byte Slot; - - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] - public byte[] MacAddress; - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct ControllerDataResponse - { - public SharedResponse Shared; - public byte Connected; - public uint PacketID; - public byte ExtraButtons; - public byte MainButtons; - public ushort PSExtraInput; - public ushort LeftStickXY; - public ushort RightStickXY; - public uint DPadAnalog; - public ulong MainButtonsAnalog; - - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] - public byte[] Touch1; - - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] - public byte[] Touch2; - public ulong MotionTimestamp; - public float AccelerometerX; - public float AccelerometerY; - public float AccelerometerZ; - public float GyroscopePitch; - public float GyroscopeYaw; - public float GyroscopeRoll; - } - - enum SubscriberType : byte - { - All = 0, - Slot, - Mac - } -} diff --git a/Ryujinx/Motion/Protocol/ControllerInfo.cs b/Ryujinx/Motion/Protocol/ControllerInfo.cs deleted file mode 100644 index 34177ff8..00000000 --- a/Ryujinx/Motion/Protocol/ControllerInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Ryujinx.Motion -{ - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct ControllerInfoResponse - { - public SharedResponse Shared; - private byte _zero; - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct ControllerInfoRequest - { - public MessageType Type; - public int PortsCount; - - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] - public byte[] PortIndices; - } -} diff --git a/Ryujinx/Motion/Protocol/Header.cs b/Ryujinx/Motion/Protocol/Header.cs deleted file mode 100644 index 1f6ea705..00000000 --- a/Ryujinx/Motion/Protocol/Header.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Ryujinx.Motion -{ - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct Header - { - public uint MagicString; - public ushort Version; - public ushort Length; - public uint Crc32; - public uint ID; - } -} diff --git a/Ryujinx/Motion/Protocol/MessageType.cs b/Ryujinx/Motion/Protocol/MessageType.cs deleted file mode 100644 index 507910dd..00000000 --- a/Ryujinx/Motion/Protocol/MessageType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ryujinx.Motion -{ - public enum MessageType : uint - { - Protocol = 0x100000, - Info, - Data - } -} diff --git a/Ryujinx/Motion/Protocol/SharedResponse.cs b/Ryujinx/Motion/Protocol/SharedResponse.cs deleted file mode 100644 index 8f918ccb..00000000 --- a/Ryujinx/Motion/Protocol/SharedResponse.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Ryujinx.Motion -{ - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct SharedResponse - { - public MessageType Type; - public byte Slot; - public SlotState State; - public DeviceModelType ModelType; - public ConnectionType ConnectionType; - - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] - public byte[] MacAddress; - public BatteryStatus BatteryStatus; - } - - public enum SlotState : byte - { - Disconnected = 0, - Reserved, - Connected - } - - public enum DeviceModelType : byte - { - None = 0, - PartialGyro, - FullGyro - } - - public enum ConnectionType : byte - { - None = 0, - USB, - Bluetooth - } - - public enum BatteryStatus : byte - { - NA = 0, - Dying, - Low, - Medium, - High, - Full, - Charging, - Charged - } -} diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs index 12db52a3..29043bb8 100644 --- a/Ryujinx/Program.cs +++ b/Ryujinx/Program.cs @@ -3,10 +3,12 @@ using Gtk; using OpenTK; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; +using Ryujinx.Common.System; using Ryujinx.Common.SystemInfo; using Ryujinx.Configuration; +using Ryujinx.Modules; using Ryujinx.Ui; -using Ryujinx.Ui.Diagnostic; +using Ryujinx.Ui.Widgets; using System; using System.IO; using System.Reflection; @@ -22,10 +24,11 @@ namespace Ryujinx static void Main(string[] args) { - // Parse Arguments - string launchPath = null; - string baseDirPath = null; - bool startFullscreen = false; + // Parse Arguments. + string launchPathArg = null; + string baseDirPathArg = null; + bool startFullscreenArg = false; + for (int i = 0; i < args.Length; ++i) { string arg = args[i]; @@ -35,28 +38,28 @@ namespace Ryujinx if (i + 1 >= args.Length) { Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'"); + continue; } - baseDirPath = args[++i]; + baseDirPathArg = args[++i]; } else if (arg == "-f" || arg == "--fullscreen") { - startFullscreen = true; + startFullscreenArg = true; } - else if (launchPath == null) + else if (launchPathArg == null) { - launchPath = arg; + launchPathArg = arg; } } - // Delete backup files after updating + // Delete backup files after updating. Task.Run(Updater.CleanupUpdate); Toolkit.Init(new ToolkitOptions { - Backend = PlatformBackend.PreferNative, - EnableHighResolution = true + Backend = PlatformBackend.PreferNative }); Version = Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion; @@ -66,27 +69,27 @@ namespace Ryujinx string systemPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine); Environment.SetEnvironmentVariable("Path", $"{Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")};{systemPath}"); - // Hook unhandled exception and process exit events - GLib.ExceptionManager.UnhandledException += (GLib.UnhandledExceptionArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating); + // Hook unhandled exception and process exit events. + GLib.ExceptionManager.UnhandledException += (GLib.UnhandledExceptionArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating); AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating); - AppDomain.CurrentDomain.ProcessExit += (object sender, EventArgs e) => ProgramExit(); + AppDomain.CurrentDomain.ProcessExit += (object sender, EventArgs e) => Exit(); - // Setup base data directory - AppDataManager.Initialize(baseDirPath); + // Setup base data directory. + AppDataManager.Initialize(baseDirPathArg); - // Initialize the configuration + // Initialize the configuration. ConfigurationState.Initialize(); - // Initialize the logger system + // Initialize the logger system. LoggerModule.Initialize(); - // Initialize Discord integration + // Initialize Discord integration. DiscordIntegrationModule.Initialize(); string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Config.json"); - string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, "Config.json"); + string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, "Config.json"); - // Now load the configuration as the other subsystems are now registered + // Now load the configuration as the other subsystems are now registered. if (File.Exists(localConfigurationPath)) { ConfigurationPath = localConfigurationPath; @@ -105,35 +108,42 @@ namespace Ryujinx } else { - // No configuration, we load the default values and save it on disk + // No configuration, we load the default values and save it on disk. ConfigurationPath = appDataConfigurationPath; ConfigurationState.Instance.LoadDefault(); ConfigurationState.Instance.ToFileFormat().SaveConfig(appDataConfigurationPath); } - if (startFullscreen) + if (startFullscreenArg) { ConfigurationState.Instance.Ui.StartFullscreen.Value = true; } + // Logging system informations. PrintSystemInfo(); + // Initialize Gtk. Application.Init(); + // Check if keys exists. bool hasGlobalProdKeys = File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys")); bool hasAltProdKeys = !AppDataManager.IsCustomBasePath && File.Exists(Path.Combine(AppDataManager.KeysDirPathAlt, "prod.keys")); - if (!hasGlobalProdKeys && !hasAltProdKeys && !Migration.IsMigrationNeeded()) + if (!hasGlobalProdKeys && !hasAltProdKeys) { UserErrorDialog.CreateUserErrorDialog(UserError.NoKeys); } + // Force dedicated GPU if we can. + ForceDedicatedGpu.Nvidia(); + + // Show the main window UI. MainWindow mainWindow = new MainWindow(); mainWindow.Show(); - if (launchPath != null) + if (launchPathArg != null) { - mainWindow.LoadApplication(launchPath); + mainWindow.LoadApplication(launchPathArg); } if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false)) @@ -147,7 +157,6 @@ namespace Ryujinx private static void PrintSystemInfo() { Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}"); - Logger.Notice.Print(LogClass.Application, $"Operating System: {SystemInfo.Instance.OsDescription}"); Logger.Notice.Print(LogClass.Application, $"CPU: {SystemInfo.Instance.CpuName}"); Logger.Notice.Print(LogClass.Application, $"Total RAM: {SystemInfo.Instance.RamSizeInMB}"); @@ -161,25 +170,30 @@ namespace Ryujinx } } - private static void ProcessUnhandledException(Exception e, bool isTerminating) + private static void ProcessUnhandledException(Exception ex, bool isTerminating) { Ptc.Close(); PtcProfiler.Stop(); - string message = $"Unhandled exception caught: {e}"; + string message = $"Unhandled exception caught: {ex}"; Logger.Error?.PrintMsg(LogClass.Application, message); - if (Logger.Error == null) Logger.Notice.PrintMsg(LogClass.Application, message); + if (Logger.Error == null) + { + Logger.Notice.PrintMsg(LogClass.Application, message); + } if (isTerminating) { - ProgramExit(); + Exit(); } } - private static void ProgramExit() + public static void Exit() { + DiscordIntegrationModule.Exit(); + Ptc.Dispose(); PtcProfiler.Dispose(); diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj index 444cee84..7f0d23e7 100644 --- a/Ryujinx/Ryujinx.csproj +++ b/Ryujinx/Ryujinx.csproj @@ -55,53 +55,51 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/Ryujinx/Ui/AboutWindow.cs b/Ryujinx/Ui/AboutWindow.cs deleted file mode 100644 index 23c0d9af..00000000 --- a/Ryujinx/Ui/AboutWindow.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Gtk; -using System; -using System.Reflection; - -using GUI = Gtk.Builder.ObjectAttribute; - -namespace Ryujinx.Ui -{ - public class AboutWindow : Window - { -#pragma warning disable CS0649 -#pragma warning disable IDE0044 - [GUI] Label _versionText; - [GUI] Image _ryujinxLogo; - [GUI] Image _patreonLogo; - [GUI] Image _gitHubLogo; - [GUI] Image _discordLogo; - [GUI] Image _twitterLogo; -#pragma warning restore CS0649 -#pragma warning restore IDE0044 - - public AboutWindow() : this(new Builder("Ryujinx.Ui.AboutWindow.glade")) { } - - private AboutWindow(Builder builder) : base(builder.GetObject("_aboutWin").Handle) - { - builder.Autoconnect(this); - - this.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); - _ryujinxLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png" , 100, 100); - _patreonLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.PatreonLogo.png", 30 , 30 ); - _gitHubLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.GitHubLogo.png" , 30 , 30 ); - _discordLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.DiscordLogo.png", 30 , 30 ); - _twitterLogo.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.TwitterLogo.png", 30 , 30 ); - - _versionText.Text = Program.Version; - } - - //Events - private void RyujinxButton_Pressed(object sender, ButtonPressEventArgs args) - { - UrlHelper.OpenUrl("https://ryujinx.org"); - } - - private void PatreonButton_Pressed(object sender, ButtonPressEventArgs args) - { - UrlHelper.OpenUrl("https://www.patreon.com/ryujinx"); - } - - private void GitHubButton_Pressed(object sender, ButtonPressEventArgs args) - { - UrlHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx"); - } - - private void DiscordButton_Pressed(object sender, ButtonPressEventArgs args) - { - UrlHelper.OpenUrl("https://discordapp.com/invite/N2FmfVc"); - } - - private void TwitterButton_Pressed(object sender, ButtonPressEventArgs args) - { - UrlHelper.OpenUrl("https://twitter.com/RyujinxEmu"); - } - - private void ContributorsButton_Pressed(object sender, ButtonPressEventArgs args) - { - UrlHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a"); - } - - private void CloseToggle_Activated(object sender, EventArgs args) - { - Dispose(); - } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/AboutWindow.glade b/Ryujinx/Ui/AboutWindow.glade deleted file mode 100644 index 8a27f372..00000000 --- a/Ryujinx/Ui/AboutWindow.glade +++ /dev/null @@ -1,574 +0,0 @@ - - - - - - False - False - True - center - 800 - 350 - dialog - - - - - - False - vertical - - - False - - - False - False - 0 - - - - - True - False - - - True - False - 10 - 15 - 10 - 15 - vertical - - - True - False - start - vertical - - - True - False - - - - True - True - 0 - - - - - True - False - center - vertical - - - True - False - Ryujinx - center - - - - - - False - True - 0 - - - - - True - False - (REE-YOU-JI-NX) - center - - - False - True - 1 - - - - - True - False - - - - True - False - Click to open the Ryujinx website in your default browser - www.ryujinx.org - center - - - - - - - - False - True - 5 - 2 - - - - - True - True - 1 - - - - - False - True - 0 - - - - - True - False - Version x.x.x (Commit Number) - center - - - False - True - 2 - 1 - - - - - True - False - MIT License - center - - - False - True - 5 - 2 - - - - - True - False - Ryujinx is not affiliated with Nintendo, -or any of its partners, in any way - center - - - - - - False - True - 5 - 3 - - - - - False - False - 0 - - - - - True - False - 25 - - - True - False - Click to open the Ryujinx Patreon page in your default browser - - - - True - False - vertical - - - - True - True - 0 - - - - - True - False - Patreon - - - False - True - 1 - - - - - - - True - True - 0 - - - - - True - False - Click to open the Ryujinx GitHub page in your default browser - - - - True - False - vertical - - - - True - True - 0 - - - - - True - False - GitHub - - - False - True - 1 - - - - - - - True - True - 1 - - - - - True - False - Click to open an invite to the Ryujinx Discord server in your default browser - - - - True - False - vertical - - - - True - True - 0 - - - - - True - False - Discord - - - False - True - 1 - - - - - - - True - True - 2 - - - - - True - False - Click to open the Ryujinx Twitter page in your default browser - - - - True - False - vertical - - - - True - True - 0 - - - - - True - False - Twitter - - - False - True - 1 - - - - - - - True - True - 3 - - - - - False - False - end - 2 - - - - - True - False - 0 - - - - - True - False - 10 - 10 - - - False - True - 1 - - - - - True - False - 15 - 10 - 40 - 15 - vertical - - - True - False - start - About - - - - - - False - True - 0 - - - - - True - False - start - 10 - Ryujinx is an emulator for the Nintendo Switch. -Please support us on Patreon. -Get all the latest news on our Twitter or Discord. -Developers interested in contributing can find out more on our Discord. - - - False - True - 5 - 1 - - - - - True - False - start - Created By: - - - - - - False - True - 5 - 2 - - - - - True - False - 10 - vertical - - - True - True - in - - - True - False - - - True - False - top - - - True - False - start - gdkchan -LDj3SNuD -Ac_K -Thog - 0 - - - True - True - 0 - - - - - True - False - start - »jD« -emmaus -Thealexbarney -Andy A (BaronKiko) - 0 - - - True - True - 1 - - - - - - - - - True - True - 0 - - - - - True - False - start - - - - True - False - end - 5 - All Contributors... - - - - - - - - False - False - 2 - - - - - True - True - 3 - - - - - True - True - 2 - - - - - True - True - 1 - - - - - - diff --git a/Ryujinx/Ui/App/ApplicationAddedEventArgs.cs b/Ryujinx/Ui/App/ApplicationAddedEventArgs.cs new file mode 100644 index 00000000..53577e67 --- /dev/null +++ b/Ryujinx/Ui/App/ApplicationAddedEventArgs.cs @@ -0,0 +1,9 @@ +using System; + +namespace Ryujinx.Ui.App +{ + public class ApplicationAddedEventArgs : EventArgs + { + public ApplicationData AppData { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/App/ApplicationCountUpdatedEventArgs.cs b/Ryujinx/Ui/App/ApplicationCountUpdatedEventArgs.cs new file mode 100644 index 00000000..510ee786 --- /dev/null +++ b/Ryujinx/Ui/App/ApplicationCountUpdatedEventArgs.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ryujinx.Ui.App +{ + public class ApplicationCountUpdatedEventArgs : EventArgs + { + public int NumAppsFound { get; set; } + public int NumAppsLoaded { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/App/ApplicationData.cs b/Ryujinx/Ui/App/ApplicationData.cs new file mode 100644 index 00000000..3940abe9 --- /dev/null +++ b/Ryujinx/Ui/App/ApplicationData.cs @@ -0,0 +1,21 @@ +using LibHac.Common; +using LibHac.Ns; + +namespace Ryujinx.Ui.App +{ + public class ApplicationData + { + public bool Favorite { get; set; } + public byte[] Icon { get; set; } + public string TitleName { get; set; } + public string TitleId { get; set; } + public string Developer { get; set; } + public string Version { get; set; } + public string TimePlayed { get; set; } + public string LastPlayed { get; set; } + public string FileExtension { get; set; } + public string FileSize { get; set; } + public string Path { get; set; } + public BlitStruct ControlHolder { get; set; } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/App/ApplicationLibrary.cs b/Ryujinx/Ui/App/ApplicationLibrary.cs new file mode 100644 index 00000000..fb0e0664 --- /dev/null +++ b/Ryujinx/Ui/App/ApplicationLibrary.cs @@ -0,0 +1,651 @@ +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; +using LibHac.Ns; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Configuration.System; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.HLE.Loaders.Npdm; +using Ryujinx.Ui.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text; +using System.Text.Json; + +using JsonHelper = Ryujinx.Common.Utilities.JsonHelper; + +namespace Ryujinx.Ui.App +{ + public class ApplicationLibrary + { + public event EventHandler ApplicationAdded; + public event EventHandler ApplicationCountUpdated; + + private readonly byte[] _nspIcon; + private readonly byte[] _xciIcon; + private readonly byte[] _ncaIcon; + private readonly byte[] _nroIcon; + private readonly byte[] _nsoIcon; + + private VirtualFileSystem _virtualFileSystem; + private Language _desiredTitleLanguage; + private bool _loadingError; + + public ApplicationLibrary(VirtualFileSystem virtualFileSystem) + { + _virtualFileSystem = virtualFileSystem; + + _nspIcon = GetResourceBytes("Ryujinx.Ui.Resources.Icon_NSP.png"); + _xciIcon = GetResourceBytes("Ryujinx.Ui.Resources.Icon_XCI.png"); + _ncaIcon = GetResourceBytes("Ryujinx.Ui.Resources.Icon_NCA.png"); + _nroIcon = GetResourceBytes("Ryujinx.Ui.Resources.Icon_NRO.png"); + _nsoIcon = GetResourceBytes("Ryujinx.Ui.Resources.Icon_NSO.png"); + } + + private byte[] GetResourceBytes(string resourceName) + { + Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName); + byte[] resourceByteArray = new byte[resourceStream.Length]; + + resourceStream.Read(resourceByteArray); + + return resourceByteArray; + } + + public IEnumerable GetFilesInDirectory(string directory) + { + Stack stack = new Stack(); + + stack.Push(directory); + + while (stack.Count > 0) + { + string dir = stack.Pop(); + string[] content = Array.Empty(); + + try + { + content = Directory.GetFiles(dir, "*"); + } + catch (UnauthorizedAccessException) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{dir}\""); + } + + if (content.Length > 0) + { + foreach (string file in content) + { + yield return file; + } + } + + try + { + content = Directory.GetDirectories(dir); + } + catch (UnauthorizedAccessException) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{dir}\""); + } + + if (content.Length > 0) + { + foreach (string subdir in content) + { + stack.Push(subdir); + } + } + } + } + + public void ReadControlData(IFileSystem controlFs, Span outProperty) + { + controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + controlFile.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); + } + + public void LoadApplications(List appDirs, Language desiredTitleLanguage) + { + int numApplicationsFound = 0; + int numApplicationsLoaded = 0; + + _loadingError = false; + _desiredTitleLanguage = desiredTitleLanguage; + + // Builds the applications list with paths to found applications + List applications = new List(); + + foreach (string appDir in appDirs) + { + + if (!Directory.Exists(appDir)) + { + Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\""); + + continue; + } + + foreach (string app in GetFilesInDirectory(appDir)) + { + if ((Path.GetExtension(app).ToLower() == ".nsp") || + (Path.GetExtension(app).ToLower() == ".pfs0") || + (Path.GetExtension(app).ToLower() == ".xci") || + (Path.GetExtension(app).ToLower() == ".nca") || + (Path.GetExtension(app).ToLower() == ".nro") || + (Path.GetExtension(app).ToLower() == ".nso")) + { + applications.Add(app); + numApplicationsFound++; + } + } + } + + // Loops through applications list, creating a struct and then firing an event containing the struct for each application + foreach (string applicationPath in applications) + { + double fileSize = new FileInfo(applicationPath).Length * 0.000000000931; + string titleName = "Unknown"; + string titleId = "0000000000000000"; + string developer = "Unknown"; + string version = "0"; + byte[] applicationIcon = null; + + BlitStruct controlHolder = new BlitStruct(1); + + try + { + using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read)) + { + if ((Path.GetExtension(applicationPath).ToLower() == ".nsp") || + (Path.GetExtension(applicationPath).ToLower() == ".pfs0") || + (Path.GetExtension(applicationPath).ToLower() == ".xci")) + { + try + { + PartitionFileSystem pfs; + + bool isExeFs = false; + + if (Path.GetExtension(applicationPath).ToLower() == ".xci") + { + Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()); + + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + pfs = new PartitionFileSystem(file.AsStorage()); + + // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. + bool hasMainNca = false; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) + { + if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") + { + pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.ContentType == NcaContentType.Program && !nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + hasMainNca = true; + + break; + } + } + else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") + { + isExeFs = true; + } + } + + if (!hasMainNca && !isExeFs) + { + numApplicationsFound--; + + continue; + } + } + + if (isExeFs) + { + applicationIcon = _nspIcon; + + Result result = pfs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read); + + if (ResultFs.PathNotFound.Includes(result)) + { + Npdm npdm = new Npdm(npdmFile.AsStream()); + + titleName = npdm.TitleName; + titleId = npdm.Aci0.TitleId.ToString("x16"); + } + } + else + { + // Store the ControlFS in variable called controlFs + GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); + + ReadControlData(controlFs, controlHolder.ByteSpan); + + // Get the title name, title ID, developer name and version number from the NACP + version = IsUpdateApplied(titleId, out string updateVersion) ? updateVersion : controlHolder.Value.DisplayVersion.ToString(); + + GetNameIdDeveloper(ref controlHolder.Value, out titleName, out _, out developer); + + // Read the icon from the ControlFS and store it as a byte array + try + { + controlFs.OpenFile(out IFile icon, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using (MemoryStream stream = new MemoryStream()) + { + icon.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + } + } + catch (HorizonResultException) + { + foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) + { + if (entry.Name == "control.nacp") + { + continue; + } + + controlFs.OpenFile(out IFile icon, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + using (MemoryStream stream = new MemoryStream()) + { + icon.AsStream().CopyTo(stream); + applicationIcon = stream.ToArray(); + } + + if (applicationIcon != null) + { + break; + } + } + + if (applicationIcon == null) + { + applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon; + } + } + } + } + catch (MissingKeyException exception) + { + applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); + } + catch (InvalidDataException) + { + applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon; + + Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); + } + catch (Exception exception) + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + Logger.Debug?.Print(LogClass.Application, exception.ToString()); + + numApplicationsFound--; + _loadingError = true; + + continue; + } + } + else if (Path.GetExtension(applicationPath).ToLower() == ".nro") + { + BinaryReader reader = new BinaryReader(file); + + byte[] Read(long position, int size) + { + file.Seek(position, SeekOrigin.Begin); + + return reader.ReadBytes(size); + } + + try + { + file.Seek(24, SeekOrigin.Begin); + + int assetOffset = reader.ReadInt32(); + + if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") + { + byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); + + long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); + long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); + + ulong nacpOffset = reader.ReadUInt64(); + ulong nacpSize = reader.ReadUInt64(); + + // Reads and stores game icon as byte array + applicationIcon = Read(assetOffset + iconOffset, (int) iconSize); + + // Read the NACP data + Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); + + // Get the title name, title ID, developer name and version number from the NACP + version = controlHolder.Value.DisplayVersion.ToString(); + + GetNameIdDeveloper(ref controlHolder.Value, out titleName, out titleId, out developer); + } + else + { + applicationIcon = _nroIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + numApplicationsFound--; + + continue; + } + } + else if (Path.GetExtension(applicationPath).ToLower() == ".nca") + { + try + { + Nca nca = new Nca(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.ContentType != NcaContentType.Program || nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + numApplicationsFound--; + + continue; + } + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); + } + catch + { + Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); + + numApplicationsFound--; + _loadingError = true; + + continue; + } + + applicationIcon = _ncaIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + // If its an NSO we just set defaults + else if (Path.GetExtension(applicationPath).ToLower() == ".nso") + { + applicationIcon = _nsoIcon; + titleName = Path.GetFileNameWithoutExtension(applicationPath); + } + } + } + catch (IOException exception) + { + Logger.Warning?.Print(LogClass.Application, exception.Message); + + numApplicationsFound--; + _loadingError = true; + + continue; + } + + ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId); + + if (appMetadata.LastPlayed != "Never" && !DateTime.TryParse(appMetadata.LastPlayed, out _)) + { + Logger.Warning?.Print(LogClass.Application, $"Last played datetime \"{appMetadata.LastPlayed}\" is invalid for current system culture, skipping (did current culture change?)"); + + appMetadata.LastPlayed = "Never"; + } + + ApplicationData data = new ApplicationData + { + Favorite = appMetadata.Favorite, + Icon = applicationIcon, + TitleName = titleName, + TitleId = titleId, + Developer = developer, + Version = version, + TimePlayed = ConvertSecondsToReadableString(appMetadata.TimePlayed), + LastPlayed = appMetadata.LastPlayed, + FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1), + FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB", + Path = applicationPath, + ControlHolder = controlHolder + }; + + numApplicationsLoaded++; + + OnApplicationAdded(new ApplicationAddedEventArgs() + { + AppData = data + }); + + OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs() + { + NumAppsFound = numApplicationsFound, + NumAppsLoaded = numApplicationsLoaded + }); + } + + OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs() + { + NumAppsFound = numApplicationsFound, + NumAppsLoaded = numApplicationsLoaded + }); + + if (_loadingError) + { + Gtk.Application.Invoke(delegate + { + GtkDialog.CreateErrorDialog("One or more files encountered could not be loaded, check logs for more info."); + }); + } + } + + protected void OnApplicationAdded(ApplicationAddedEventArgs e) + { + ApplicationAdded?.Invoke(null, e); + } + + protected void OnApplicationCountUpdated(ApplicationCountUpdatedEventArgs e) + { + ApplicationCountUpdated?.Invoke(null, e); + } + + private void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId) + { + (_, _, Nca controlNca) = ApplicationLoader.GetGameData(_virtualFileSystem, pfs, 0); + + // Return the ControlFS + controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); + titleId = controlNca?.Header.TitleId.ToString("x16"); + } + + internal ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) + { + string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); + string metadataFile = Path.Combine(metadataFolder, "metadata.json"); + + ApplicationMetadata appMetadata; + + if (!File.Exists(metadataFile)) + { + Directory.CreateDirectory(metadataFolder); + + appMetadata = new ApplicationMetadata(); + + using (FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough)) + { + JsonHelper.Serialize(stream, appMetadata, true); + } + } + + try + { + appMetadata = JsonHelper.DeserializeFromFile(metadataFile); + } + catch (JsonException) + { + Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults."); + + appMetadata = new ApplicationMetadata(); + } + + if (modifyFunction != null) + { + modifyFunction(appMetadata); + + using (FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough)) + { + JsonHelper.Serialize(stream, appMetadata, true); + } + } + + return appMetadata; + } + + private string ConvertSecondsToReadableString(double seconds) + { + const int secondsPerMinute = 60; + const int secondsPerHour = secondsPerMinute * 60; + const int secondsPerDay = secondsPerHour * 24; + + string readableString; + + if (seconds < secondsPerMinute) + { + readableString = $"{seconds}s"; + } + else if (seconds < secondsPerHour) + { + readableString = $"{Math.Round(seconds / secondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins"; + } + else if (seconds < secondsPerDay) + { + readableString = $"{Math.Round(seconds / secondsPerHour, 2, MidpointRounding.AwayFromZero)} hrs"; + } + else + { + readableString = $"{Math.Round(seconds / secondsPerDay, 2, MidpointRounding.AwayFromZero)} days"; + } + + return readableString; + } + + private void GetNameIdDeveloper(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher) + { + _ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); + + if (controlData.Titles.Length > (int)desiredTitleLanguage) + { + titleName = controlData.Titles[(int)desiredTitleLanguage].Name.ToString(); + publisher = controlData.Titles[(int)desiredTitleLanguage].Publisher.ToString(); + } + else + { + titleName = null; + publisher = null; + } + + if (string.IsNullOrWhiteSpace(titleName)) + { + foreach (ApplicationControlTitle controlTitle in controlData.Titles) + { + if (!((U8Span)controlTitle.Name).IsEmpty()) + { + titleName = controlTitle.Name.ToString(); + + break; + } + } + } + + if (string.IsNullOrWhiteSpace(publisher)) + { + foreach (ApplicationControlTitle controlTitle in controlData.Titles) + { + if (!((U8Span)controlTitle.Publisher).IsEmpty()) + { + publisher = controlTitle.Publisher.ToString(); + + break; + } + } + } + + if (controlData.PresenceGroupId != 0) + { + titleId = controlData.PresenceGroupId.ToString("x16"); + } + else if (controlData.SaveDataOwnerId.Value != 0) + { + titleId = controlData.SaveDataOwnerId.ToString(); + } + else if (controlData.AddOnContentBaseId != 0) + { + titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); + } + else + { + titleId = "0000000000000000"; + } + } + + private bool IsUpdateApplied(string titleId, out string version) + { + string updatePath = "(unknown)"; + + try + { + (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); + + if (patchNca != null && controlNca != null) + { + ApplicationControlProperty controlData = new ApplicationControlProperty(); + + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + + version = controlData.DisplayVersion.ToString(); + + return true; + } + } + catch (InvalidDataException) + { + Logger.Warning?.Print(LogClass.Application, + $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}"); + } + catch (MissingKeyException exception) + { + Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}"); + } + + version = ""; + + return false; + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/App/ApplicationMetadata.cs b/Ryujinx/Ui/App/ApplicationMetadata.cs new file mode 100644 index 00000000..2bd87c0e --- /dev/null +++ b/Ryujinx/Ui/App/ApplicationMetadata.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.Ui.App +{ + public class ApplicationMetadata + { + public bool Favorite { get; set; } + public double TimePlayed { get; set; } + public string LastPlayed { get; set; } = "Never"; + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Applet/ErrorAppletDialog.cs b/Ryujinx/Ui/Applet/ErrorAppletDialog.cs new file mode 100644 index 00000000..a51d5324 --- /dev/null +++ b/Ryujinx/Ui/Applet/ErrorAppletDialog.cs @@ -0,0 +1,27 @@ +using Gtk; + +namespace Ryujinx.Ui.Applet +{ + internal class ErrorAppletDialog : MessageDialog + { + public ErrorAppletDialog(Window parentWindow, DialogFlags dialogFlags, MessageType messageType, string[] buttons) : base(parentWindow, dialogFlags, messageType, ButtonsType.None, null) + { + int responseId = 0; + + if (buttons != null) + { + foreach (string buttonText in buttons) + { + AddButton(buttonText, responseId); + responseId++; + } + } + else + { + AddButton("OK", 0); + } + + ShowAll(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Applet/GtkHostUiHandler.cs b/Ryujinx/Ui/Applet/GtkHostUiHandler.cs new file mode 100644 index 00000000..804a1a27 --- /dev/null +++ b/Ryujinx/Ui/Applet/GtkHostUiHandler.cs @@ -0,0 +1,190 @@ +using Gtk; +using Ryujinx.HLE; +using Ryujinx.HLE.HOS.Applets; +using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; +using Ryujinx.Ui.Widgets; +using System; +using System.Threading; + +namespace Ryujinx.Ui.Applet +{ + internal class GtkHostUiHandler : IHostUiHandler + { + private readonly Window _parent; + + public GtkHostUiHandler(Window parent) + { + _parent = parent; + } + + public bool DisplayMessageDialog(ControllerAppletUiArgs args) + { + string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}"; + + string message = $"Application requests {playerCount} player(s) with:\n\n" + + $"TYPES: {args.SupportedStyles}\n\n" + + $"PLAYERS: {string.Join(", ", args.SupportedPlayers)}\n\n" + + (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : "") + + "Please reconfigure Input now and then press OK."; + + return DisplayMessageDialog("Controller Applet", message); + } + + public bool DisplayMessageDialog(string title, string message) + { + ManualResetEvent dialogCloseEvent = new ManualResetEvent(false); + + bool okPressed = false; + + Application.Invoke(delegate + { + MessageDialog msgDialog = null; + + try + { + msgDialog = new MessageDialog(_parent, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null) + { + Title = title, + Text = message, + UseMarkup = true + }; + + msgDialog.SetDefaultSize(400, 0); + + msgDialog.Response += (object o, ResponseArgs args) => + { + if (args.ResponseId == ResponseType.Ok) + { + okPressed = true; + } + + dialogCloseEvent.Set(); + msgDialog?.Dispose(); + }; + + msgDialog.Show(); + } + catch (Exception ex) + { + GtkDialog.CreateErrorDialog($"Error displaying Message Dialog: {ex}"); + + dialogCloseEvent.Set(); + } + }); + + dialogCloseEvent.WaitOne(); + + return okPressed; + } + + public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) + { + ManualResetEvent dialogCloseEvent = new ManualResetEvent(false); + + bool okPressed = false; + bool error = false; + string inputText = args.InitialText ?? ""; + + Application.Invoke(delegate + { + try + { + var swkbdDialog = new SwkbdAppletDialog(_parent) + { + Title = "Software Keyboard", + Text = args.HeaderText, + SecondaryText = args.SubtitleText + }; + + swkbdDialog.InputEntry.Text = inputText; + swkbdDialog.InputEntry.PlaceholderText = args.GuideText; + swkbdDialog.OkButton.Label = args.SubmitText; + + swkbdDialog.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax); + + if (swkbdDialog.Run() == (int)ResponseType.Ok) + { + inputText = swkbdDialog.InputEntry.Text; + okPressed = true; + } + + swkbdDialog.Dispose(); + } + catch (Exception ex) + { + error = true; + + GtkDialog.CreateErrorDialog($"Error displaying Software Keyboard: {ex}"); + } + finally + { + dialogCloseEvent.Set(); + } + }); + + dialogCloseEvent.WaitOne(); + + userText = error ? null : inputText; + + return error || okPressed; + } + + public void ExecuteProgram(HLE.Switch device, ProgramSpecifyKind kind, ulong value) + { + device.UserChannelPersistence.ExecuteProgram(kind, value); + ((MainWindow)_parent).GlRendererWidget?.Exit(); + } + + public bool DisplayErrorAppletDialog(string title, string message, string[] buttons) + { + ManualResetEvent dialogCloseEvent = new ManualResetEvent(false); + + bool showDetails = false; + + Application.Invoke(delegate + { + try + { + ErrorAppletDialog msgDialog = new ErrorAppletDialog(_parent, DialogFlags.DestroyWithParent, MessageType.Error, buttons) + { + Title = title, + Text = message, + UseMarkup = true, + WindowPosition = WindowPosition.CenterAlways + }; + + msgDialog.SetDefaultSize(400, 0); + + msgDialog.Response += (object o, ResponseArgs args) => + { + if (buttons != null) + { + if (buttons.Length > 1) + { + if (args.ResponseId != (ResponseType)(buttons.Length - 1)) + { + showDetails = true; + } + } + } + + dialogCloseEvent.Set(); + msgDialog?.Dispose(); + }; + + msgDialog.Show(); + } + catch (Exception ex) + { + GtkDialog.CreateErrorDialog($"Error displaying ErrorApplet Dialog: {ex}"); + + dialogCloseEvent.Set(); + } + }); + + dialogCloseEvent.WaitOne(); + + return showDetails; + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs b/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs new file mode 100644 index 00000000..7c14f0e8 --- /dev/null +++ b/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs @@ -0,0 +1,89 @@ +using Gtk; +using System; + +namespace Ryujinx.Ui.Applet +{ + public class SwkbdAppletDialog : MessageDialog + { + private int _inputMin; + private int _inputMax; + + private Predicate _checkLength; + + private readonly Label _validationInfo; + + public Entry InputEntry { get; } + public Button OkButton { get; } + public Button CancelButton { get; } + + public SwkbdAppletDialog(Window parent) : base(parent, DialogFlags.Modal | DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.None, null) + { + SetDefaultSize(300, 0); + + _validationInfo = new Label() + { + Visible = false + }; + + InputEntry = new Entry() + { + Visible = true + }; + + InputEntry.Activated += OnInputActivated; + InputEntry.Changed += OnInputChanged; + + OkButton = (Button)AddButton("OK", ResponseType.Ok); + CancelButton = (Button)AddButton("Cancel", ResponseType.Cancel); + + ((Box)MessageArea).PackEnd(_validationInfo, true, true, 0); + ((Box)MessageArea).PackEnd(InputEntry, true, true, 4); + + SetInputLengthValidation(0, int.MaxValue); // Disable by default. + } + + public void SetInputLengthValidation(int min, int max) + { + _inputMin = Math.Min(min, max); + _inputMax = Math.Max(min, max); + + _validationInfo.Visible = false; + + if (_inputMin <= 0 && _inputMax == int.MaxValue) // Disable. + { + _validationInfo.Visible = false; + + _checkLength = (length) => true; + } + else if (_inputMin > 0 && _inputMax == int.MaxValue) + { + _validationInfo.Visible = true; + _validationInfo.Markup = $"Must be at least {_inputMin} characters long"; + + _checkLength = (length) => _inputMin <= length; + } + else + { + _validationInfo.Visible = true; + _validationInfo.Markup = $"Must be {_inputMin}-{_inputMax} characters long"; + + _checkLength = (length) => _inputMin <= length && length <= _inputMax; + } + + OnInputChanged(this, EventArgs.Empty); + } + + private void OnInputActivated(object sender, EventArgs e) + { + if (OkButton.IsSensitive) + { + Respond(ResponseType.Ok); + } + } + + private void OnInputChanged(object sender, EventArgs e) + { + OkButton.Sensitive = _checkLength(InputEntry.Text.Length); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/ApplicationAddedEventArgs.cs b/Ryujinx/Ui/ApplicationAddedEventArgs.cs deleted file mode 100644 index 5e09389b..00000000 --- a/Ryujinx/Ui/ApplicationAddedEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Ryujinx.Ui -{ - public class ApplicationAddedEventArgs : EventArgs - { - public ApplicationData AppData { get; set; } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/ApplicationCountUpdatedEventArgs.cs b/Ryujinx/Ui/ApplicationCountUpdatedEventArgs.cs deleted file mode 100644 index 78e2e50c..00000000 --- a/Ryujinx/Ui/ApplicationCountUpdatedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Ryujinx.Ui -{ - public class ApplicationCountUpdatedEventArgs : EventArgs - { - public int NumAppsFound { get; set; } - public int NumAppsLoaded { get; set; } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/ApplicationData.cs b/Ryujinx/Ui/ApplicationData.cs deleted file mode 100644 index 34784c6c..00000000 --- a/Ryujinx/Ui/ApplicationData.cs +++ /dev/null @@ -1,22 +0,0 @@ -using LibHac; -using LibHac.Common; -using LibHac.Ns; - -namespace Ryujinx.Ui -{ - public struct ApplicationData - { - public bool Favorite { get; set; } - public byte[] Icon { get; set; } - public string TitleName { get; set; } - public string TitleId { get; set; } - public string Developer { get; set; } - public string Version { get; set; } - public string TimePlayed { get; set; } - public string LastPlayed { get; set; } - public string FileExtension { get; set; } - public string FileSize { get; set; } - public string Path { get; set; } - public BlitStruct ControlHolder { get; set; } - } -} diff --git a/Ryujinx/Ui/ApplicationLibrary.cs b/Ryujinx/Ui/ApplicationLibrary.cs deleted file mode 100644 index 353b686e..00000000 --- a/Ryujinx/Ui/ApplicationLibrary.cs +++ /dev/null @@ -1,650 +0,0 @@ -using LibHac; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.FsSystem; -using LibHac.FsSystem.NcaUtils; -using LibHac.Ns; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; -using Ryujinx.Configuration.System; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS; -using Ryujinx.HLE.Loaders.Npdm; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text; -using System.Text.Json; - -using JsonHelper = Ryujinx.Common.Utilities.JsonHelper; - -namespace Ryujinx.Ui -{ - public class ApplicationLibrary - { - public static event EventHandler ApplicationAdded; - public static event EventHandler ApplicationCountUpdated; - - private static readonly byte[] _nspIcon = GetResourceBytes("Ryujinx.Ui.assets.NSPIcon.png"); - private static readonly byte[] _xciIcon = GetResourceBytes("Ryujinx.Ui.assets.XCIIcon.png"); - private static readonly byte[] _ncaIcon = GetResourceBytes("Ryujinx.Ui.assets.NCAIcon.png"); - private static readonly byte[] _nroIcon = GetResourceBytes("Ryujinx.Ui.assets.NROIcon.png"); - private static readonly byte[] _nsoIcon = GetResourceBytes("Ryujinx.Ui.assets.NSOIcon.png"); - - private static VirtualFileSystem _virtualFileSystem; - private static Language _desiredTitleLanguage; - private static bool _loadingError; - - public static IEnumerable GetFilesInDirectory(string directory) - { - Stack stack = new Stack(); - - stack.Push(directory); - - while (stack.Count > 0) - { - string dir = stack.Pop(); - string[] content = { }; - - try - { - content = Directory.GetFiles(dir, "*"); - } - catch (UnauthorizedAccessException) - { - Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{dir}\""); - } - - if (content.Length > 0) - { - foreach (string file in content) - { - yield return file; - } - } - - try - { - content = Directory.GetDirectories(dir); - } - catch (UnauthorizedAccessException) - { - Logger.Warning?.Print(LogClass.Application, $"Failed to get access to directory: \"{dir}\""); - } - - if (content.Length > 0) - { - foreach (string subdir in content) - { - stack.Push(subdir); - } - } - } - } - - public static void ReadControlData(IFileSystem controlFs, Span outProperty) - { - controlFs.OpenFile(out IFile controlFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - controlFile.Read(out _, 0, outProperty, ReadOption.None).ThrowIfFailure(); - } - - public static void LoadApplications(List appDirs, VirtualFileSystem virtualFileSystem, Language desiredTitleLanguage) - { - int numApplicationsFound = 0; - int numApplicationsLoaded = 0; - - _loadingError = false; - _virtualFileSystem = virtualFileSystem; - _desiredTitleLanguage = desiredTitleLanguage; - - // Builds the applications list with paths to found applications - List applications = new List(); - - foreach (string appDir in appDirs) - { - - if (!Directory.Exists(appDir)) - { - Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"Config.json\" contains an invalid directory: \"{appDir}\""); - - continue; - } - - foreach (string app in GetFilesInDirectory(appDir)) - { - if ((Path.GetExtension(app).ToLower() == ".nsp") || - (Path.GetExtension(app).ToLower() == ".pfs0") || - (Path.GetExtension(app).ToLower() == ".xci") || - (Path.GetExtension(app).ToLower() == ".nca") || - (Path.GetExtension(app).ToLower() == ".nro") || - (Path.GetExtension(app).ToLower() == ".nso")) - { - applications.Add(app); - numApplicationsFound++; - } - } - } - - // Loops through applications list, creating a struct and then firing an event containing the struct for each application - foreach (string applicationPath in applications) - { - double fileSize = new FileInfo(applicationPath).Length * 0.000000000931; - string titleName = "Unknown"; - string titleId = "0000000000000000"; - string developer = "Unknown"; - string version = "0"; - byte[] applicationIcon = null; - - BlitStruct controlHolder = new BlitStruct(1); - - try - { - using (FileStream file = new FileStream(applicationPath, FileMode.Open, FileAccess.Read)) - { - if ((Path.GetExtension(applicationPath).ToLower() == ".nsp") || - (Path.GetExtension(applicationPath).ToLower() == ".pfs0") || - (Path.GetExtension(applicationPath).ToLower() == ".xci")) - { - try - { - PartitionFileSystem pfs; - - bool isExeFs = false; - - if (Path.GetExtension(applicationPath).ToLower() == ".xci") - { - Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()); - - pfs = xci.OpenPartition(XciPartitionType.Secure); - } - else - { - pfs = new PartitionFileSystem(file.AsStorage()); - - // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application. - bool hasMainNca = false; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*")) - { - if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca") - { - pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - if (nca.Header.ContentType == NcaContentType.Program && !nca.Header.GetFsHeader(dataIndex).IsPatchSection()) - { - hasMainNca = true; - - break; - } - } - else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main") - { - isExeFs = true; - } - } - - if (!hasMainNca && !isExeFs) - { - numApplicationsFound--; - - continue; - } - } - - if (isExeFs) - { - applicationIcon = _nspIcon; - - Result result = pfs.OpenFile(out IFile npdmFile, "/main.npdm".ToU8Span(), OpenMode.Read); - - if (ResultFs.PathNotFound.Includes(result)) - { - Npdm npdm = new Npdm(npdmFile.AsStream()); - - titleName = npdm.TitleName; - titleId = npdm.Aci0.TitleId.ToString("x16"); - } - } - else - { - // Store the ControlFS in variable called controlFs - GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId); - - ReadControlData(controlFs, controlHolder.ByteSpan); - - // Get the title name, title ID, developer name and version number from the NACP - version = IsUpdateApplied(titleId, out string updateVersion) ? updateVersion : controlHolder.Value.DisplayVersion.ToString(); - - GetNameIdDeveloper(ref controlHolder.Value, out titleName, out _, out developer); - - // Read the icon from the ControlFS and store it as a byte array - try - { - controlFs.OpenFile(out IFile icon, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using (MemoryStream stream = new MemoryStream()) - { - icon.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - } - } - catch (HorizonResultException) - { - foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*")) - { - if (entry.Name == "control.nacp") - { - continue; - } - - controlFs.OpenFile(out IFile icon, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - using (MemoryStream stream = new MemoryStream()) - { - icon.AsStream().CopyTo(stream); - applicationIcon = stream.ToArray(); - } - - if (applicationIcon != null) - { - break; - } - } - - if (applicationIcon == null) - { - applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon; - } - } - } - } - catch (MissingKeyException exception) - { - applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon; - - Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}"); - } - catch (InvalidDataException) - { - applicationIcon = Path.GetExtension(applicationPath).ToLower() == ".xci" ? _xciIcon : _nspIcon; - - Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}"); - } - catch (Exception exception) - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - Logger.Debug?.Print(LogClass.Application, exception.ToString()); - - numApplicationsFound--; - _loadingError = true; - - continue; - } - } - else if (Path.GetExtension(applicationPath).ToLower() == ".nro") - { - BinaryReader reader = new BinaryReader(file); - - byte[] Read(long position, int size) - { - file.Seek(position, SeekOrigin.Begin); - - return reader.ReadBytes(size); - } - - try - { - file.Seek(24, SeekOrigin.Begin); - - int assetOffset = reader.ReadInt32(); - - if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET") - { - byte[] iconSectionInfo = Read(assetOffset + 8, 0x10); - - long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0); - long iconSize = BitConverter.ToInt64(iconSectionInfo, 8); - - ulong nacpOffset = reader.ReadUInt64(); - ulong nacpSize = reader.ReadUInt64(); - - // Reads and stores game icon as byte array - applicationIcon = Read(assetOffset + iconOffset, (int) iconSize); - - // Read the NACP data - Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan); - - // Get the title name, title ID, developer name and version number from the NACP - version = controlHolder.Value.DisplayVersion.ToString(); - - GetNameIdDeveloper(ref controlHolder.Value, out titleName, out titleId, out developer); - } - else - { - applicationIcon = _nroIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); - } - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - numApplicationsFound--; - - continue; - } - } - else if (Path.GetExtension(applicationPath).ToLower() == ".nca") - { - try - { - Nca nca = new Nca(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage()); - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - if (nca.Header.ContentType != NcaContentType.Program || nca.Header.GetFsHeader(dataIndex).IsPatchSection()) - { - numApplicationsFound--; - - continue; - } - } - catch (InvalidDataException) - { - Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}"); - } - catch - { - Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}"); - - numApplicationsFound--; - _loadingError = true; - - continue; - } - - applicationIcon = _ncaIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); - } - // If its an NSO we just set defaults - else if (Path.GetExtension(applicationPath).ToLower() == ".nso") - { - applicationIcon = _nsoIcon; - titleName = Path.GetFileNameWithoutExtension(applicationPath); - } - } - } - catch (IOException exception) - { - Logger.Warning?.Print(LogClass.Application, exception.Message); - - numApplicationsFound--; - _loadingError = true; - - continue; - } - - ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId); - - if (appMetadata.LastPlayed != "Never" && !DateTime.TryParse(appMetadata.LastPlayed, out _)) - { - Logger.Warning?.Print(LogClass.Application, $"Last played datetime \"{appMetadata.LastPlayed}\" is invalid for current system culture, skipping (did current culture change?)"); - - appMetadata.LastPlayed = "Never"; - } - - ApplicationData data = new ApplicationData - { - Favorite = appMetadata.Favorite, - Icon = applicationIcon, - TitleName = titleName, - TitleId = titleId, - Developer = developer, - Version = version, - TimePlayed = ConvertSecondsToReadableString(appMetadata.TimePlayed), - LastPlayed = appMetadata.LastPlayed, - FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1), - FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + "MB" : fileSize.ToString("0.##") + "GB", - Path = applicationPath, - ControlHolder = controlHolder - }; - - numApplicationsLoaded++; - - OnApplicationAdded(new ApplicationAddedEventArgs() - { - AppData = data - }); - - OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs() - { - NumAppsFound = numApplicationsFound, - NumAppsLoaded = numApplicationsLoaded - }); - } - - OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs() - { - NumAppsFound = numApplicationsFound, - NumAppsLoaded = numApplicationsLoaded - }); - - if (_loadingError) - { - Gtk.Application.Invoke(delegate - { - GtkDialog.CreateErrorDialog("One or more files encountered could not be loaded, check logs for more info."); - }); - } - } - - protected static void OnApplicationAdded(ApplicationAddedEventArgs e) - { - ApplicationAdded?.Invoke(null, e); - } - - protected static void OnApplicationCountUpdated(ApplicationCountUpdatedEventArgs e) - { - ApplicationCountUpdated?.Invoke(null, e); - } - - private static byte[] GetResourceBytes(string resourceName) - { - Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName); - byte[] resourceByteArray = new byte[resourceStream.Length]; - - resourceStream.Read(resourceByteArray); - - return resourceByteArray; - } - - private static void GetControlFsAndTitleId(PartitionFileSystem pfs, out IFileSystem controlFs, out string titleId) - { - (_, _, Nca controlNca) = ApplicationLoader.GetGameData(_virtualFileSystem, pfs, 0); - - // Return the ControlFS - controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None); - titleId = controlNca?.Header.TitleId.ToString("x16"); - } - - internal static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null) - { - string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui"); - string metadataFile = Path.Combine(metadataFolder, "metadata.json"); - - ApplicationMetadata appMetadata; - - if (!File.Exists(metadataFile)) - { - Directory.CreateDirectory(metadataFolder); - - appMetadata = new ApplicationMetadata - { - Favorite = false, - TimePlayed = 0, - LastPlayed = "Never" - }; - - using (FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough)) - { - JsonHelper.Serialize(stream, appMetadata, true); - } - } - - try - { - appMetadata = JsonHelper.DeserializeFromFile(metadataFile); - } - catch (JsonException) - { - Logger.Warning?.Print(LogClass.Application, $"Failed to parse metadata json for {titleId}. Loading defaults."); - - appMetadata = new ApplicationMetadata - { - Favorite = false, - TimePlayed = 0, - LastPlayed = "Never" - }; - } - - if (modifyFunction != null) - { - modifyFunction(appMetadata); - - using (FileStream stream = File.Create(metadataFile, 4096, FileOptions.WriteThrough)) - { - JsonHelper.Serialize(stream, appMetadata, true); - } - } - - return appMetadata; - } - - private static string ConvertSecondsToReadableString(double seconds) - { - const int secondsPerMinute = 60; - const int secondsPerHour = secondsPerMinute * 60; - const int secondsPerDay = secondsPerHour * 24; - - string readableString; - - if (seconds < secondsPerMinute) - { - readableString = $"{seconds}s"; - } - else if (seconds < secondsPerHour) - { - readableString = $"{Math.Round(seconds / secondsPerMinute, 2, MidpointRounding.AwayFromZero)} mins"; - } - else if (seconds < secondsPerDay) - { - readableString = $"{Math.Round(seconds / secondsPerHour, 2, MidpointRounding.AwayFromZero)} hrs"; - } - else - { - readableString = $"{Math.Round(seconds / secondsPerDay, 2, MidpointRounding.AwayFromZero)} days"; - } - - return readableString; - } - - private static void GetNameIdDeveloper(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher) - { - Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage); - - if (controlData.Titles.Length > (int)desiredTitleLanguage) - { - titleName = controlData.Titles[(int)desiredTitleLanguage].Name.ToString(); - publisher = controlData.Titles[(int)desiredTitleLanguage].Publisher.ToString(); - } - else - { - titleName = null; - publisher = null; - } - - if (string.IsNullOrWhiteSpace(titleName)) - { - foreach (ApplicationControlTitle controlTitle in controlData.Titles) - { - if (!((U8Span)controlTitle.Name).IsEmpty()) - { - titleName = controlTitle.Name.ToString(); - - break; - } - } - } - - if (string.IsNullOrWhiteSpace(publisher)) - { - foreach (ApplicationControlTitle controlTitle in controlData.Titles) - { - if (!((U8Span)controlTitle.Publisher).IsEmpty()) - { - publisher = controlTitle.Publisher.ToString(); - - break; - } - } - } - - if (controlData.PresenceGroupId != 0) - { - titleId = controlData.PresenceGroupId.ToString("x16"); - } - else if (controlData.SaveDataOwnerId.Value != 0) - { - titleId = controlData.SaveDataOwnerId.ToString(); - } - else if (controlData.AddOnContentBaseId != 0) - { - titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16"); - } - else - { - titleId = "0000000000000000"; - } - } - - private static bool IsUpdateApplied(string titleId, out string version) - { - string updatePath = "(unknown)"; - - try - { - (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath); - - if (patchNca != null && controlNca != null) - { - ApplicationControlProperty controlData = new ApplicationControlProperty(); - - controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - - version = controlData.DisplayVersion.ToString(); - - return true; - } - } - catch (InvalidDataException) - { - Logger.Warning?.Print(LogClass.Application, - $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {updatePath}"); - } - catch (MissingKeyException exception) - { - Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {updatePath}"); - } - - version = ""; - - return false; - } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/ApplicationMetadata.cs b/Ryujinx/Ui/ApplicationMetadata.cs deleted file mode 100644 index cdedf91b..00000000 --- a/Ryujinx/Ui/ApplicationMetadata.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ryujinx.Ui -{ - internal class ApplicationMetadata - { - public bool Favorite { get; set; } - public double TimePlayed { get; set; } - public string LastPlayed { get; set; } - } -} diff --git a/Ryujinx/Ui/ControllerWindow.cs b/Ryujinx/Ui/ControllerWindow.cs deleted file mode 100644 index 77c8db0c..00000000 --- a/Ryujinx/Ui/ControllerWindow.cs +++ /dev/null @@ -1,1034 +0,0 @@ -using Gtk; -using OpenTK.Input; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Utilities; -using Ryujinx.Configuration; -using Ryujinx.HLE.FileSystem; -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Text.Json; -using System.Threading; - -using GUI = Gtk.Builder.ObjectAttribute; -using Key = Ryujinx.Configuration.Hid.Key; - -namespace Ryujinx.Ui -{ - public class ControllerWindow : Window - { - private PlayerIndex _playerIndex; - private InputConfig _inputConfig; - private bool _isWaitingForInput; - private VirtualFileSystem _virtualFileSystem; - -#pragma warning disable CS0649, IDE0044 - [GUI] Adjustment _controllerDeadzoneLeft; - [GUI] Adjustment _controllerDeadzoneRight; - [GUI] Adjustment _controllerTriggerThreshold; - [GUI] Adjustment _slotNumber; - [GUI] Adjustment _altSlotNumber; - [GUI] Adjustment _sensitivity; - [GUI] Adjustment _gyroDeadzone; - [GUI] CheckButton _enableMotion; - [GUI] CheckButton _mirrorInput; - [GUI] Entry _dsuServerHost; - [GUI] Entry _dsuServerPort; - [GUI] ComboBoxText _inputDevice; - [GUI] ComboBoxText _profile; - [GUI] ToggleButton _refreshInputDevicesButton; - [GUI] Box _settingsBox; - [GUI] Box _altBox; - [GUI] Grid _leftStickKeyboard; - [GUI] Grid _leftStickController; - [GUI] Box _deadZoneLeftBox; - [GUI] Grid _rightStickKeyboard; - [GUI] Grid _rightStickController; - [GUI] Box _deadZoneRightBox; - [GUI] Grid _leftSideTriggerBox; - [GUI] Grid _rightSideTriggerBox; - [GUI] Box _triggerThresholdBox; - [GUI] ComboBoxText _controllerType; - [GUI] ToggleButton _lStickX; - [GUI] CheckButton _invertLStickX; - [GUI] ToggleButton _lStickY; - [GUI] CheckButton _invertLStickY; - [GUI] ToggleButton _lStickUp; - [GUI] ToggleButton _lStickDown; - [GUI] ToggleButton _lStickLeft; - [GUI] ToggleButton _lStickRight; - [GUI] ToggleButton _lStickButton; - [GUI] ToggleButton _dpadUp; - [GUI] ToggleButton _dpadDown; - [GUI] ToggleButton _dpadLeft; - [GUI] ToggleButton _dpadRight; - [GUI] ToggleButton _minus; - [GUI] ToggleButton _l; - [GUI] ToggleButton _zL; - [GUI] ToggleButton _rStickX; - [GUI] CheckButton _invertRStickX; - [GUI] ToggleButton _rStickY; - [GUI] CheckButton _invertRStickY; - [GUI] ToggleButton _rStickUp; - [GUI] ToggleButton _rStickDown; - [GUI] ToggleButton _rStickLeft; - [GUI] ToggleButton _rStickRight; - [GUI] ToggleButton _rStickButton; - [GUI] ToggleButton _a; - [GUI] ToggleButton _b; - [GUI] ToggleButton _x; - [GUI] ToggleButton _y; - [GUI] ToggleButton _plus; - [GUI] ToggleButton _r; - [GUI] ToggleButton _zR; - [GUI] ToggleButton _lSl; - [GUI] ToggleButton _lSr; - [GUI] ToggleButton _rSl; - [GUI] ToggleButton _rSr; - [GUI] Image _controllerImage; -#pragma warning restore CS0649, IDE0044 - - public ControllerWindow(PlayerIndex controllerId, VirtualFileSystem virtualFileSystem) : this(new Builder("Ryujinx.Ui.ControllerWindow.glade"), controllerId, virtualFileSystem) { } - - private ControllerWindow(Builder builder, PlayerIndex controllerId, VirtualFileSystem virtualFileSystem) : base(builder.GetObject("_controllerWin").Handle) - { - builder.Autoconnect(this); - - this.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); - - _playerIndex = controllerId; - _virtualFileSystem = virtualFileSystem; - _inputConfig = ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerIndex); - - Title = $"Ryujinx - Controller Settings - {_playerIndex}"; - - if (_playerIndex == PlayerIndex.Handheld) - { - _controllerType.Append(ControllerType.Handheld.ToString(), "Handheld"); - _controllerType.Sensitive = false; - } - else - { - _controllerType.Append(ControllerType.ProController.ToString(), "Pro Controller"); - _controllerType.Append(ControllerType.JoyconPair.ToString(), "Joycon Pair"); - _controllerType.Append(ControllerType.JoyconLeft.ToString(), "Joycon Left"); - _controllerType.Append(ControllerType.JoyconRight.ToString(), "Joycon Right"); - } - - _controllerType.Active = 0; // Set initial value to first in list. - - //Bind Events - _lStickX.Clicked += Button_Pressed; - _lStickY.Clicked += Button_Pressed; - _lStickUp.Clicked += Button_Pressed; - _lStickDown.Clicked += Button_Pressed; - _lStickLeft.Clicked += Button_Pressed; - _lStickRight.Clicked += Button_Pressed; - _lStickButton.Clicked += Button_Pressed; - _dpadUp.Clicked += Button_Pressed; - _dpadDown.Clicked += Button_Pressed; - _dpadLeft.Clicked += Button_Pressed; - _dpadRight.Clicked += Button_Pressed; - _minus.Clicked += Button_Pressed; - _l.Clicked += Button_Pressed; - _zL.Clicked += Button_Pressed; - _lSl.Clicked += Button_Pressed; - _lSr.Clicked += Button_Pressed; - _rStickX.Clicked += Button_Pressed; - _rStickY.Clicked += Button_Pressed; - _rStickUp.Clicked += Button_Pressed; - _rStickDown.Clicked += Button_Pressed; - _rStickLeft.Clicked += Button_Pressed; - _rStickRight.Clicked += Button_Pressed; - _rStickButton.Clicked += Button_Pressed; - _a.Clicked += Button_Pressed; - _b.Clicked += Button_Pressed; - _x.Clicked += Button_Pressed; - _y.Clicked += Button_Pressed; - _plus.Clicked += Button_Pressed; - _r.Clicked += Button_Pressed; - _zR.Clicked += Button_Pressed; - _rSl.Clicked += Button_Pressed; - _rSr.Clicked += Button_Pressed; - - // Setup current values - UpdateInputDeviceList(); - SetAvailableOptions(); - - ClearValues(); - if (_inputDevice.ActiveId != null) SetCurrentValues(); - } - - private void UpdateInputDeviceList() - { - _inputDevice.RemoveAll(); - _inputDevice.Append("disabled", "Disabled"); - _inputDevice.SetActiveId("disabled"); - - _inputDevice.Append($"keyboard/{KeyboardConfig.AllKeyboardsIndex}", "All keyboards"); - - for (int i = 0; i < 20; i++) - { - if (KeyboardController.GetKeyboardState(i + 1).IsConnected) - _inputDevice.Append($"keyboard/{i + 1}", $"Keyboard/{i + 1}"); - - if (GamePad.GetState(i).IsConnected) - _inputDevice.Append($"controller/{i}", $"Controller/{i} ({GamePad.GetName(i)})"); - } - - switch (_inputConfig) - { - case KeyboardConfig keyboard: - _inputDevice.SetActiveId($"keyboard/{keyboard.Index}"); - break; - case ControllerConfig controller: - _inputDevice.SetActiveId($"controller/{controller.Index}"); - break; - } - } - - private void SetAvailableOptions() - { - if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("keyboard")) - { - this.ShowAll(); - _leftStickController.Hide(); - _rightStickController.Hide(); - _deadZoneLeftBox.Hide(); - _deadZoneRightBox.Hide(); - _triggerThresholdBox.Hide(); - } - else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller")) - { - this.ShowAll(); - _leftStickKeyboard.Hide(); - _rightStickKeyboard.Hide(); - } - else - { - _settingsBox.Hide(); - } - - ClearValues(); - } - - private void SetCurrentValues() - { - SetControllerSpecificFields(); - - SetProfiles(); - - if (_inputDevice.ActiveId.StartsWith("keyboard") && _inputConfig is KeyboardConfig) - { - SetValues(_inputConfig); - } - else if (_inputDevice.ActiveId.StartsWith("controller") && _inputConfig is ControllerConfig) - { - SetValues(_inputConfig); - } - } - - private void SetControllerSpecificFields() - { - _leftSideTriggerBox.Hide(); - _rightSideTriggerBox.Hide(); - _altBox.Hide(); - - switch (_controllerType.ActiveId) - { - case "JoyconLeft": - _leftSideTriggerBox.Show(); - break; - case "JoyconRight": - _rightSideTriggerBox.Show(); - break; - case "JoyconPair": - _altBox.Show(); - break; - } - - switch (_controllerType.ActiveId) - { - case "ProController": - _controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.ProCon.svg", 400, 400); - break; - case "JoyconLeft": - _controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.JoyConLeft.svg", 400, 400); - break; - case "JoyconRight": - _controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.JoyConRight.svg", 400, 400); - break; - default: - _controllerImage.Pixbuf = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.JoyConPair.svg", 400, 400); - break; - } - } - - private void ClearValues() - { - _lStickX.Label = "Unbound"; - _lStickY.Label = "Unbound"; - _lStickUp.Label = "Unbound"; - _lStickDown.Label = "Unbound"; - _lStickLeft.Label = "Unbound"; - _lStickRight.Label = "Unbound"; - _lStickButton.Label = "Unbound"; - _dpadUp.Label = "Unbound"; - _dpadDown.Label = "Unbound"; - _dpadLeft.Label = "Unbound"; - _dpadRight.Label = "Unbound"; - _minus.Label = "Unbound"; - _l.Label = "Unbound"; - _zL.Label = "Unbound"; - _lSl.Label = "Unbound"; - _lSr.Label = "Unbound"; - _rStickX.Label = "Unbound"; - _rStickY.Label = "Unbound"; - _rStickUp.Label = "Unbound"; - _rStickDown.Label = "Unbound"; - _rStickLeft.Label = "Unbound"; - _rStickRight.Label = "Unbound"; - _rStickButton.Label = "Unbound"; - _a.Label = "Unbound"; - _b.Label = "Unbound"; - _x.Label = "Unbound"; - _y.Label = "Unbound"; - _plus.Label = "Unbound"; - _r.Label = "Unbound"; - _zR.Label = "Unbound"; - _rSl.Label = "Unbound"; - _rSr.Label = "Unbound"; - _controllerDeadzoneLeft.Value = 0; - _controllerDeadzoneRight.Value = 0; - _controllerTriggerThreshold.Value = 0; - _mirrorInput.Active = false; - _enableMotion.Active = false; - _slotNumber.Value = 0; - _altSlotNumber.Value = 0; - _sensitivity.Value = 100; - _gyroDeadzone.Value = 1; - _dsuServerHost.Buffer.Text = ""; - _dsuServerPort.Buffer.Text = ""; - } - - private void SetValues(InputConfig config) - { - switch (config) - { - case KeyboardConfig keyboardConfig: - if (!_controllerType.SetActiveId(keyboardConfig.ControllerType.ToString())) - { - _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld - ? ControllerType.Handheld.ToString() - : ControllerType.ProController.ToString()); - } - - _lStickUp.Label = keyboardConfig.LeftJoycon.StickUp.ToString(); - _lStickDown.Label = keyboardConfig.LeftJoycon.StickDown.ToString(); - _lStickLeft.Label = keyboardConfig.LeftJoycon.StickLeft.ToString(); - _lStickRight.Label = keyboardConfig.LeftJoycon.StickRight.ToString(); - _lStickButton.Label = keyboardConfig.LeftJoycon.StickButton.ToString(); - _dpadUp.Label = keyboardConfig.LeftJoycon.DPadUp.ToString(); - _dpadDown.Label = keyboardConfig.LeftJoycon.DPadDown.ToString(); - _dpadLeft.Label = keyboardConfig.LeftJoycon.DPadLeft.ToString(); - _dpadRight.Label = keyboardConfig.LeftJoycon.DPadRight.ToString(); - _minus.Label = keyboardConfig.LeftJoycon.ButtonMinus.ToString(); - _l.Label = keyboardConfig.LeftJoycon.ButtonL.ToString(); - _zL.Label = keyboardConfig.LeftJoycon.ButtonZl.ToString(); - _lSl.Label = keyboardConfig.LeftJoycon.ButtonSl.ToString(); - _lSr.Label = keyboardConfig.LeftJoycon.ButtonSr.ToString(); - _rStickUp.Label = keyboardConfig.RightJoycon.StickUp.ToString(); - _rStickDown.Label = keyboardConfig.RightJoycon.StickDown.ToString(); - _rStickLeft.Label = keyboardConfig.RightJoycon.StickLeft.ToString(); - _rStickRight.Label = keyboardConfig.RightJoycon.StickRight.ToString(); - _rStickButton.Label = keyboardConfig.RightJoycon.StickButton.ToString(); - _a.Label = keyboardConfig.RightJoycon.ButtonA.ToString(); - _b.Label = keyboardConfig.RightJoycon.ButtonB.ToString(); - _x.Label = keyboardConfig.RightJoycon.ButtonX.ToString(); - _y.Label = keyboardConfig.RightJoycon.ButtonY.ToString(); - _plus.Label = keyboardConfig.RightJoycon.ButtonPlus.ToString(); - _r.Label = keyboardConfig.RightJoycon.ButtonR.ToString(); - _zR.Label = keyboardConfig.RightJoycon.ButtonZr.ToString(); - _rSl.Label = keyboardConfig.RightJoycon.ButtonSl.ToString(); - _rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString(); - _slotNumber.Value = keyboardConfig.Slot; - _altSlotNumber.Value = keyboardConfig.AltSlot; - _sensitivity.Value = keyboardConfig.Sensitivity; - _gyroDeadzone.Value = keyboardConfig.GyroDeadzone; - _enableMotion.Active = keyboardConfig.EnableMotion; - _mirrorInput.Active = keyboardConfig.MirrorInput; - _dsuServerHost.Buffer.Text = keyboardConfig.DsuServerHost; - _dsuServerPort.Buffer.Text = keyboardConfig.DsuServerPort.ToString(); - break; - case ControllerConfig controllerConfig: - if (!_controllerType.SetActiveId(controllerConfig.ControllerType.ToString())) - { - _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld - ? ControllerType.Handheld.ToString() - : ControllerType.ProController.ToString()); - } - - _lStickX.Label = controllerConfig.LeftJoycon.StickX.ToString(); - _invertLStickX.Active = controllerConfig.LeftJoycon.InvertStickX; - _lStickY.Label = controllerConfig.LeftJoycon.StickY.ToString(); - _invertLStickY.Active = controllerConfig.LeftJoycon.InvertStickY; - _lStickButton.Label = controllerConfig.LeftJoycon.StickButton.ToString(); - _dpadUp.Label = controllerConfig.LeftJoycon.DPadUp.ToString(); - _dpadDown.Label = controllerConfig.LeftJoycon.DPadDown.ToString(); - _dpadLeft.Label = controllerConfig.LeftJoycon.DPadLeft.ToString(); - _dpadRight.Label = controllerConfig.LeftJoycon.DPadRight.ToString(); - _minus.Label = controllerConfig.LeftJoycon.ButtonMinus.ToString(); - _l.Label = controllerConfig.LeftJoycon.ButtonL.ToString(); - _zL.Label = controllerConfig.LeftJoycon.ButtonZl.ToString(); - _lSl.Label = controllerConfig.LeftJoycon.ButtonSl.ToString(); - _lSr.Label = controllerConfig.LeftJoycon.ButtonSr.ToString(); - _rStickX.Label = controllerConfig.RightJoycon.StickX.ToString(); - _invertRStickX.Active = controllerConfig.RightJoycon.InvertStickX; - _rStickY.Label = controllerConfig.RightJoycon.StickY.ToString(); - _invertRStickY.Active = controllerConfig.RightJoycon.InvertStickY; - _rStickButton.Label = controllerConfig.RightJoycon.StickButton.ToString(); - _a.Label = controllerConfig.RightJoycon.ButtonA.ToString(); - _b.Label = controllerConfig.RightJoycon.ButtonB.ToString(); - _x.Label = controllerConfig.RightJoycon.ButtonX.ToString(); - _y.Label = controllerConfig.RightJoycon.ButtonY.ToString(); - _plus.Label = controllerConfig.RightJoycon.ButtonPlus.ToString(); - _r.Label = controllerConfig.RightJoycon.ButtonR.ToString(); - _zR.Label = controllerConfig.RightJoycon.ButtonZr.ToString(); - _rSl.Label = controllerConfig.RightJoycon.ButtonSl.ToString(); - _rSr.Label = controllerConfig.RightJoycon.ButtonSr.ToString(); - _controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft; - _controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight; - _controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold; - _slotNumber.Value = controllerConfig.Slot; - _altSlotNumber.Value = controllerConfig.AltSlot; - _sensitivity.Value = controllerConfig.Sensitivity; - _gyroDeadzone.Value = controllerConfig.GyroDeadzone; - _enableMotion.Active = controllerConfig.EnableMotion; - _mirrorInput.Active = controllerConfig.MirrorInput; - _dsuServerHost.Buffer.Text = controllerConfig.DsuServerHost; - _dsuServerPort.Buffer.Text = controllerConfig.DsuServerPort.ToString(); - break; - } - } - - private InputConfig GetValues() - { - if (_inputDevice.ActiveId.StartsWith("keyboard")) - { - Enum.TryParse(_lStickUp.Label, out Key lStickUp); - Enum.TryParse(_lStickDown.Label, out Key lStickDown); - Enum.TryParse(_lStickLeft.Label, out Key lStickLeft); - Enum.TryParse(_lStickRight.Label, out Key lStickRight); - Enum.TryParse(_lStickButton.Label, out Key lStickButton); - Enum.TryParse(_dpadUp.Label, out Key lDPadUp); - Enum.TryParse(_dpadDown.Label, out Key lDPadDown); - Enum.TryParse(_dpadLeft.Label, out Key lDPadLeft); - Enum.TryParse(_dpadRight.Label, out Key lDPadRight); - Enum.TryParse(_minus.Label, out Key lButtonMinus); - Enum.TryParse(_l.Label, out Key lButtonL); - Enum.TryParse(_zL.Label, out Key lButtonZl); - Enum.TryParse(_lSl.Label, out Key lButtonSl); - Enum.TryParse(_lSr.Label, out Key lButtonSr); - - Enum.TryParse(_rStickUp.Label, out Key rStickUp); - Enum.TryParse(_rStickDown.Label, out Key rStickDown); - Enum.TryParse(_rStickLeft.Label, out Key rStickLeft); - Enum.TryParse(_rStickRight.Label, out Key rStickRight); - Enum.TryParse(_rStickButton.Label, out Key rStickButton); - Enum.TryParse(_a.Label, out Key rButtonA); - Enum.TryParse(_b.Label, out Key rButtonB); - Enum.TryParse(_x.Label, out Key rButtonX); - Enum.TryParse(_y.Label, out Key rButtonY); - Enum.TryParse(_plus.Label, out Key rButtonPlus); - Enum.TryParse(_r.Label, out Key rButtonR); - Enum.TryParse(_zR.Label, out Key rButtonZr); - Enum.TryParse(_rSl.Label, out Key rButtonSl); - Enum.TryParse(_rSr.Label, out Key rButtonSr); - - int.TryParse(_dsuServerPort.Buffer.Text, out int port); - - return new KeyboardConfig - { - Index = int.Parse(_inputDevice.ActiveId.Split("/")[1]), - ControllerType = Enum.Parse(_controllerType.ActiveId), - PlayerIndex = _playerIndex, - LeftJoycon = new NpadKeyboardLeft - { - StickUp = lStickUp, - StickDown = lStickDown, - StickLeft = lStickLeft, - StickRight = lStickRight, - StickButton = lStickButton, - DPadUp = lDPadUp, - DPadDown = lDPadDown, - DPadLeft = lDPadLeft, - DPadRight = lDPadRight, - ButtonMinus = lButtonMinus, - ButtonL = lButtonL, - ButtonZl = lButtonZl, - ButtonSl = lButtonSl, - ButtonSr = lButtonSr - }, - RightJoycon = new NpadKeyboardRight - { - StickUp = rStickUp, - StickDown = rStickDown, - StickLeft = rStickLeft, - StickRight = rStickRight, - StickButton = rStickButton, - ButtonA = rButtonA, - ButtonB = rButtonB, - ButtonX = rButtonX, - ButtonY = rButtonY, - ButtonPlus = rButtonPlus, - ButtonR = rButtonR, - ButtonZr = rButtonZr, - ButtonSl = rButtonSl, - ButtonSr = rButtonSr - }, - EnableMotion = _enableMotion.Active, - MirrorInput = _mirrorInput.Active, - Slot = (int)_slotNumber.Value, - AltSlot = (int)_altSlotNumber.Value, - Sensitivity = (int)_sensitivity.Value, - GyroDeadzone = _gyroDeadzone.Value, - DsuServerHost = _dsuServerHost.Buffer.Text, - DsuServerPort = port - }; - } - - if (_inputDevice.ActiveId.StartsWith("controller")) - { - Enum.TryParse(_lStickX.Label, out ControllerInputId lStickX); - Enum.TryParse(_lStickY.Label, out ControllerInputId lStickY); - Enum.TryParse(_lStickButton.Label, out ControllerInputId lStickButton); - Enum.TryParse(_minus.Label, out ControllerInputId lButtonMinus); - Enum.TryParse(_l.Label, out ControllerInputId lButtonL); - Enum.TryParse(_zL.Label, out ControllerInputId lButtonZl); - Enum.TryParse(_lSl.Label, out ControllerInputId lButtonSl); - Enum.TryParse(_lSr.Label, out ControllerInputId lButtonSr); - Enum.TryParse(_dpadUp.Label, out ControllerInputId lDPadUp); - Enum.TryParse(_dpadDown.Label, out ControllerInputId lDPadDown); - Enum.TryParse(_dpadLeft.Label, out ControllerInputId lDPadLeft); - Enum.TryParse(_dpadRight.Label, out ControllerInputId lDPadRight); - - Enum.TryParse(_rStickX.Label, out ControllerInputId rStickX); - Enum.TryParse(_rStickY.Label, out ControllerInputId rStickY); - Enum.TryParse(_rStickButton.Label, out ControllerInputId rStickButton); - Enum.TryParse(_a.Label, out ControllerInputId rButtonA); - Enum.TryParse(_b.Label, out ControllerInputId rButtonB); - Enum.TryParse(_x.Label, out ControllerInputId rButtonX); - Enum.TryParse(_y.Label, out ControllerInputId rButtonY); - Enum.TryParse(_plus.Label, out ControllerInputId rButtonPlus); - Enum.TryParse(_r.Label, out ControllerInputId rButtonR); - Enum.TryParse(_zR.Label, out ControllerInputId rButtonZr); - Enum.TryParse(_rSl.Label, out ControllerInputId rButtonSl); - Enum.TryParse(_rSr.Label, out ControllerInputId rButtonSr); - - int.TryParse(_dsuServerPort.Buffer.Text, out int port); - - return new ControllerConfig - { - Index = int.Parse(_inputDevice.ActiveId.Split("/")[1]), - ControllerType = Enum.Parse(_controllerType.ActiveId), - PlayerIndex = _playerIndex, - DeadzoneLeft = (float)_controllerDeadzoneLeft.Value, - DeadzoneRight = (float)_controllerDeadzoneRight.Value, - TriggerThreshold = (float)_controllerTriggerThreshold.Value, - LeftJoycon = new NpadControllerLeft - { - InvertStickX = _invertLStickX.Active, - StickX = lStickX, - InvertStickY = _invertLStickY.Active, - StickY = lStickY, - StickButton = lStickButton, - ButtonMinus = lButtonMinus, - ButtonL = lButtonL, - ButtonZl = lButtonZl, - ButtonSl = lButtonSl, - ButtonSr = lButtonSr, - DPadUp = lDPadUp, - DPadDown = lDPadDown, - DPadLeft = lDPadLeft, - DPadRight = lDPadRight - }, - RightJoycon = new NpadControllerRight - { - InvertStickX = _invertRStickX.Active, - StickX = rStickX, - InvertStickY = _invertRStickY.Active, - StickY = rStickY, - StickButton = rStickButton, - ButtonA = rButtonA, - ButtonB = rButtonB, - ButtonX = rButtonX, - ButtonY = rButtonY, - ButtonPlus = rButtonPlus, - ButtonR = rButtonR, - ButtonZr = rButtonZr, - ButtonSl = rButtonSl, - ButtonSr = rButtonSr - }, - EnableMotion = _enableMotion.Active, - MirrorInput = _mirrorInput.Active, - Slot = (int)_slotNumber.Value, - AltSlot = (int)_altSlotNumber.Value, - Sensitivity = (int)_sensitivity.Value, - GyroDeadzone = _gyroDeadzone.Value, - DsuServerHost = _dsuServerHost.Buffer.Text, - DsuServerPort = port - }; - } - - if (!_inputDevice.ActiveId.StartsWith("disabled")) - { - GtkDialog.CreateErrorDialog("Some fields entered where invalid and therefore your config was not saved."); - } - - return null; - } - - private static bool IsAnyKeyPressed(out Key pressedKey, int index) - { - KeyboardState keyboardState = KeyboardController.GetKeyboardState(index); - - foreach (Key key in Enum.GetValues(typeof(Key))) - { - if (keyboardState.IsKeyDown((OpenTK.Input.Key)key)) - { - pressedKey = key; - - return true; - } - } - - pressedKey = Key.Unbound; - - return false; - } - - private static bool IsAnyButtonPressed(out ControllerInputId pressedButton, int index, double triggerThreshold) - { - JoystickState joystickState = Joystick.GetState(index); - JoystickCapabilities joystickCapabilities = Joystick.GetCapabilities(index); - - //Buttons - for (int i = 0; i != joystickCapabilities.ButtonCount; i++) - { - if (joystickState.IsButtonDown(i)) - { - Enum.TryParse($"Button{i}", out pressedButton); - - return true; - } - } - - //Axis - for (int i = 0; i != joystickCapabilities.AxisCount; i++) - { - if (joystickState.GetAxis(i) > 0.5f && joystickState.GetAxis(i) > triggerThreshold) - { - Enum.TryParse($"Axis{i}", out pressedButton); - - return true; - } - } - - //Hats - for (int i = 0; i != joystickCapabilities.HatCount; i++) - { - JoystickHatState hatState = joystickState.GetHat((JoystickHat)i); - string pos = null; - - if (hatState.IsUp) pos = "Up"; - if (hatState.IsDown) pos = "Down"; - if (hatState.IsLeft) pos = "Left"; - if (hatState.IsRight) pos = "Right"; - if (pos == null) continue; - - Enum.TryParse($"Hat{i}{pos}", out pressedButton); - - return true; - } - - pressedButton = ControllerInputId.Unbound; - - return false; - } - - private string GetProfileBasePath() - { - string path = AppDataManager.ProfilesDirPath; - - if (_inputDevice.ActiveId.StartsWith("keyboard")) - { - path = System.IO.Path.Combine(path, "keyboard"); - } - else if (_inputDevice.ActiveId.StartsWith("controller")) - { - path = System.IO.Path.Combine(path, "controller"); - } - - return path; - } - - //Events - private void InputDevice_Changed(object sender, EventArgs args) - { - SetAvailableOptions(); - SetControllerSpecificFields(); - - if (_inputDevice.ActiveId != null) SetProfiles(); - } - - private void Controller_Changed(object sender, EventArgs args) - { - SetControllerSpecificFields(); - } - - private void RefreshInputDevicesButton_Pressed(object sender, EventArgs args) - { - UpdateInputDeviceList(); - - _refreshInputDevicesButton.SetStateFlags(0, true); - } - - private void Button_Pressed(object sender, EventArgs args) - { - if (_isWaitingForInput) - { - return; - } - - _isWaitingForInput = true; - - Thread inputThread = new Thread(() => - { - Button button = (ToggleButton)sender; - - if (_inputDevice.ActiveId.StartsWith("keyboard")) - { - Key pressedKey; - - int index = int.Parse(_inputDevice.ActiveId.Split("/")[1]); - while (!IsAnyKeyPressed(out pressedKey, index)) - { - if (Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsKeyDown(OpenTK.Input.Key.Escape)) - { - Application.Invoke(delegate - { - button.SetStateFlags(0, true); - }); - - _isWaitingForInput = false; - - return; - } - } - - Application.Invoke(delegate - { - button.Label = pressedKey.ToString(); - button.SetStateFlags(0, true); - }); - } - else if (_inputDevice.ActiveId.StartsWith("controller")) - { - ControllerInputId pressedButton; - - int index = int.Parse(_inputDevice.ActiveId.Split("/")[1]); - while (!IsAnyButtonPressed(out pressedButton, index, _controllerTriggerThreshold.Value)) - { - if (Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsAnyKeyDown) - { - Application.Invoke(delegate - { - button.SetStateFlags(0, true); - }); - - _isWaitingForInput = false; - - return; - } - } - - Application.Invoke(delegate - { - button.Label = pressedButton.ToString(); - button.SetStateFlags(0, true); - }); - } - - _isWaitingForInput = false; - }); - inputThread.Name = "GUI.InputThread"; - inputThread.IsBackground = true; - inputThread.Start(); - } - - private void SetProfiles() - { - string basePath = GetProfileBasePath(); - - if (!Directory.Exists(basePath)) - { - Directory.CreateDirectory(basePath); - } - - _profile.RemoveAll(); - _profile.Append("default", "Default"); - - foreach (string profile in Directory.GetFiles(basePath, "*.*", SearchOption.AllDirectories)) - { - _profile.Append(System.IO.Path.GetFileName(profile), System.IO.Path.GetFileNameWithoutExtension(profile)); - } - } - - private void ProfileLoad_Activated(object sender, EventArgs args) - { - ((ToggleButton)sender).SetStateFlags(0, true); - - if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == null) return; - - InputConfig config = null; - int pos = _profile.Active; - - if (_profile.ActiveId == "default") - { - if (_inputDevice.ActiveId.StartsWith("keyboard")) - { - config = new KeyboardConfig - { - Index = 0, - ControllerType = ControllerType.JoyconPair, - LeftJoycon = new NpadKeyboardLeft - { - StickUp = Key.W, - StickDown = Key.S, - StickLeft = Key.A, - StickRight = Key.D, - StickButton = Key.F, - DPadUp = Key.Up, - DPadDown = Key.Down, - DPadLeft = Key.Left, - DPadRight = Key.Right, - ButtonMinus = Key.Minus, - ButtonL = Key.E, - ButtonZl = Key.Q, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound - }, - RightJoycon = new NpadKeyboardRight - { - StickUp = Key.I, - StickDown = Key.K, - StickLeft = Key.J, - StickRight = Key.L, - StickButton = Key.H, - ButtonA = Key.Z, - ButtonB = Key.X, - ButtonX = Key.C, - ButtonY = Key.V, - ButtonPlus = Key.Plus, - ButtonR = Key.U, - ButtonZr = Key.O, - ButtonSl = Key.Unbound, - ButtonSr = Key.Unbound - }, - EnableMotion = false, - MirrorInput = false, - Slot = 0, - AltSlot = 0, - Sensitivity = 100, - GyroDeadzone = 1, - DsuServerHost = "127.0.0.1", - DsuServerPort = 26760 - }; - } - else if (_inputDevice.ActiveId.StartsWith("controller")) - { - config = new ControllerConfig - { - Index = 0, - ControllerType = ControllerType.ProController, - DeadzoneLeft = 0.1f, - DeadzoneRight = 0.1f, - TriggerThreshold = 0.5f, - LeftJoycon = new NpadControllerLeft - { - StickX = ControllerInputId.Axis0, - StickY = ControllerInputId.Axis1, - StickButton = ControllerInputId.Button8, - DPadUp = ControllerInputId.Hat0Up, - DPadDown = ControllerInputId.Hat0Down, - DPadLeft = ControllerInputId.Hat0Left, - DPadRight = ControllerInputId.Hat0Right, - ButtonMinus = ControllerInputId.Button6, - ButtonL = ControllerInputId.Button4, - ButtonZl = ControllerInputId.Axis2, - ButtonSl = ControllerInputId.Unbound, - ButtonSr = ControllerInputId.Unbound, - InvertStickX = false, - InvertStickY = false - }, - RightJoycon = new NpadControllerRight - { - StickX = ControllerInputId.Axis3, - StickY = ControllerInputId.Axis4, - StickButton = ControllerInputId.Button9, - ButtonA = ControllerInputId.Button1, - ButtonB = ControllerInputId.Button0, - ButtonX = ControllerInputId.Button3, - ButtonY = ControllerInputId.Button2, - ButtonPlus = ControllerInputId.Button7, - ButtonR = ControllerInputId.Button5, - ButtonZr = ControllerInputId.Axis5, - ButtonSl = ControllerInputId.Unbound, - ButtonSr = ControllerInputId.Unbound, - InvertStickX = false, - InvertStickY = false - }, - EnableMotion = false, - MirrorInput = false, - Slot = 0, - AltSlot = 0, - Sensitivity = 100, - GyroDeadzone = 1, - DsuServerHost = "127.0.0.1", - DsuServerPort = 26760 - }; - } - } - else - { - string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId); - - if (!File.Exists(path)) - { - if (pos >= 0) - { - _profile.Remove(pos); - } - - return; - } - - try - { - using (Stream stream = File.OpenRead(path)) - { - config = JsonHelper.Deserialize(stream); - } - } - catch (JsonException) - { - try - { - using (Stream stream = File.OpenRead(path)) - { - config = JsonHelper.Deserialize(stream); - } - } - catch { } - } - } - - SetValues(config); - } - - private void ProfileAdd_Activated(object sender, EventArgs args) - { - ((ToggleButton)sender).SetStateFlags(0, true); - - if (_inputDevice.ActiveId == "disabled") return; - - InputConfig inputConfig = GetValues(); - ProfileDialog profileDialog = new ProfileDialog(); - - if (inputConfig == null) return; - - if (profileDialog.Run() == (int)ResponseType.Ok) - { - string path = System.IO.Path.Combine(GetProfileBasePath(), profileDialog.FileName); - string jsonString; - - if (inputConfig is KeyboardConfig keyboardConfig) - { - jsonString = JsonHelper.Serialize(keyboardConfig, true); - } - else - { - jsonString = JsonHelper.Serialize(inputConfig as ControllerConfig, true); - } - - File.WriteAllText(path, jsonString); - } - - profileDialog.Dispose(); - - SetProfiles(); - } - - private void ProfileRemove_Activated(object sender, EventArgs args) - { - ((ToggleButton) sender).SetStateFlags(0, true); - - if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == "default" || _profile.ActiveId == null) return; - - MessageDialog confirmDialog = GtkDialog.CreateConfirmationDialog("Deleting Profile", "This action is irreversible, are your sure you want to continue?"); - - if (confirmDialog.Run() == (int)ResponseType.Yes) - { - string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId); - - if (File.Exists(path)) - { - File.Delete(path); - } - - SetProfiles(); - } - } - - private void SaveToggle_Activated(object sender, EventArgs args) - { - InputConfig inputConfig = GetValues(); - - var newConfig = new List(); - newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); - - if (_inputConfig == null && inputConfig != null) - { - newConfig.Add(inputConfig); - } - else - { - if (_inputDevice.ActiveId == "disabled") - { - newConfig.Remove(_inputConfig); - } - else if (inputConfig != null) - { - int index = newConfig.IndexOf(_inputConfig); - - newConfig[index] = inputConfig; - } - } - - // Atomically replace and signal input change. - // NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event. - ConfigurationState.Instance.Hid.InputConfig.Value = newConfig; - - MainWindow.SaveConfig(); - - Dispose(); - } - - private void CloseToggle_Activated(object sender, EventArgs args) - { - Dispose(); - } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/ControllerWindow.glade b/Ryujinx/Ui/ControllerWindow.glade deleted file mode 100644 index 2143e9de..00000000 --- a/Ryujinx/Ui/ControllerWindow.glade +++ /dev/null @@ -1,2038 +0,0 @@ - - - - - - 4 - 1 - 4 - - - 1 - 0.050000000000000003 - 0.01 - 0.10000000000000001 - - - 1 - 0.050000000000000003 - 0.01 - 0.10000000000000001 - - - 1 - 0.5 - 0.01 - 0.10000000000000001 - - - 100 - 0.01 - 0.01 - 0.10000000000000001 - 0.10000000000000001 - - - 1000 - 100 - 1 - 4 - - - 4 - 1 - 4 - - - False - Ryujinx - Controller Settings - True - center - 1100 - 600 - - - - - - True - False - vertical - - - True - True - in - - - True - False - - - True - False - vertical - - - True - False - 10 - 10 - 10 - - - True - False - - - True - False - 5 - Input Device - - - False - True - 0 - - - - - True - False - 0 - disabled - - Disabled - - - - - True - True - 1 - - - - - Refresh - True - True - True - 5 - - - - False - True - 2 - - - - - False - True - 0 - - - - - True - False - 20 - - - True - False - The controller's type - center - 5 - Controller Type: - - - False - True - 0 - - - - - True - False - The controller's type - 0 - - - - False - True - 1 - - - - - False - True - 1 - - - - - True - False - 20 - - - True - False - 5 - Profile: - - - False - True - 0 - - - - - True - False - 5 - 0 - default - - - False - True - 1 - - - - - Load - 60 - True - True - True - 5 - - - - False - True - 2 - - - - - Add - 60 - True - True - True - 5 - - - - False - True - 3 - - - - - Remove - 60 - True - True - True - - - - False - True - 4 - - - - - False - True - 2 - - - - - False - True - 0 - - - - - True - False - - - True - False - vertical - - - True - False - 10 - 5 - - - 156 - True - False - 10 - vertical - - - True - False - 5 - 5 - Buttons - - - - - - False - True - 0 - - - - - True - False - 3 - 10 - - - 80 - True - False - A - - - 0 - 0 - - - - - 80 - True - False - B - - - 0 - 1 - - - - - 80 - True - False - X - - - 0 - 2 - - - - - 80 - True - False - Y - - - 0 - 3 - - - - - - 70 - True - True - True - - - 1 - 0 - - - - - - 70 - True - True - True - - - 1 - 1 - - - - - - 70 - True - True - True - - - 1 - 2 - - - - - - 70 - True - True - True - - - 1 - 3 - - - - - 80 - True - False - + - - - 0 - 4 - - - - - 80 - True - False - - - - - 0 - 5 - - - - - - 70 - True - True - True - - - 1 - 5 - - - - - - 70 - True - True - True - - - 1 - 4 - - - - - False - True - 1 - - - - - False - True - 0 - - - - - True - False - - - False - True - 1 - - - - - 160 - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - Left Stick - - - - - - False - True - 0 - - - - - True - False - 5 - 3 - 10 - - - 80 - True - False - LStick Button - 0 - - - 0 - 0 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - False - True - 1 - - - - - True - False - 3 - 10 - - - - 65 - True - True - True - - - 1 - 1 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 2 - - - - - - 65 - True - True - True - - - 1 - 3 - - - - - 80 - True - False - LStick Down - 0 - - - 0 - 1 - - - - - 80 - True - False - LStick Up - 0 - - - 0 - 0 - - - - - 80 - True - False - LStick Right - 0 - - - 0 - 3 - - - - - 80 - True - False - LStick Left - 0 - - - 0 - 2 - - - - - False - True - 2 - - - - - True - False - 3 - 10 - - - 80 - True - False - LStick Lt/Rt - 0 - - - 0 - 0 - - - - - 80 - True - False - LStick Up/Dn - 0 - - - 0 - 1 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 1 - - - - - Invert - True - True - False - True - - - 2 - 0 - - - - - Invert - True - True - False - True - - - 2 - 1 - - - - - False - True - 3 - - - - - True - False - 10 - vertical - - - True - False - start - Deadzone Left - - - False - True - 0 - - - - - True - True - _controllerDeadzoneLeft - 2 - 2 - - - True - True - 1 - - - - - False - True - 4 - - - - - False - True - 2 - - - - - True - False - - - False - True - 3 - - - - - 150 - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - Triggers - - - - - - False - True - 0 - - - - - True - False - 3 - 10 - - - 80 - True - False - L - - - 0 - 0 - - - - - 80 - True - False - R - - - 0 - 1 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 1 - - - - - 80 - True - False - ZL - - - 0 - 2 - - - - - 80 - True - False - ZR - - - 0 - 3 - - - - - - 65 - True - True - True - - - 1 - 2 - - - - - - 65 - True - True - True - - - 1 - 3 - - - - - False - True - 1 - - - - - _sideTriggerBox - True - False - 5 - 3 - 10 - - - 80 - True - False - Left SL - - - 0 - 0 - - - - - 80 - True - False - Left SR - - - 0 - 1 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 1 - - - - - False - True - 2 - - - - - _sideTriggerBox - True - False - 5 - 3 - 10 - - - 80 - True - False - Right SL - - - 0 - 0 - - - - - 80 - True - False - Right SR - - - 0 - 1 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 1 - - - - - False - True - 3 - - - - - True - False - 10 - vertical - - - True - False - start - 10 - Trigger Threshold - - - False - True - 0 - - - - - True - True - _controllerTriggerThreshold - 2 - 2 - - - True - True - 1 - - - - - False - True - 4 - - - - - False - True - 4 - - - - - False - True - 0 - - - - - True - False - 10 - - - 156 - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - Directional Pad - - - - - - False - True - 0 - - - - - True - False - 3 - 10 - - - 80 - True - False - Dpad Up - 0 - - - 0 - 0 - - - - - 80 - True - False - Dpad Down - 0 - - - 0 - 1 - - - - - 80 - True - False - Dpad Left - 0 - - - 0 - 2 - - - - - 80 - True - False - Dpad Right - 0 - - - 0 - 3 - - - - - - 70 - True - True - True - - - 1 - 0 - - - - - - 70 - True - True - True - - - 1 - 1 - - - - - - 70 - True - True - True - - - 1 - 2 - - - - - - 70 - True - True - True - - - 1 - 3 - - - - - False - True - 1 - - - - - False - True - 0 - - - - - True - False - - - False - True - 1 - - - - - 160 - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - Right Stick - - - - - - False - True - 0 - - - - - True - False - 5 - 3 - 10 - - - 80 - True - False - RStick Button - 0 - - - 0 - 0 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - False - True - 1 - - - - - True - False - 3 - 10 - - - 80 - True - False - RStick Up - 0 - - - 0 - 0 - - - - - 80 - True - False - RStick Down - 0 - - - 0 - 1 - - - - - 80 - True - False - RStick Left - 0 - - - 0 - 2 - - - - - 80 - True - False - RStick Right - 0 - - - 0 - 3 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 1 - - - - - - 65 - True - True - True - - - 1 - 2 - - - - - - 65 - True - True - True - - - 1 - 3 - - - - - False - True - 2 - - - - - True - False - 3 - 10 - - - 80 - True - False - RStick Lt/Rt - 0 - - - 0 - 0 - - - - - 80 - True - False - RStick Up/Dn - 0 - - - 0 - 1 - - - - - - 65 - True - True - True - - - 1 - 0 - - - - - - 65 - True - True - True - - - 1 - 1 - - - - - Invert - True - True - False - True - - - 2 - 0 - - - - - Invert - True - True - False - True - - - 2 - 1 - - - - - False - True - 3 - - - - - True - False - 10 - vertical - - - True - False - start - Deadzone Right - - - False - True - 0 - - - - - True - True - _controllerDeadzoneRight - 2 - 2 - - - True - True - 1 - - - - - False - True - 4 - - - - - False - True - 2 - - - - - True - False - - - False - True - 3 - - - - - True - False - 10 - 10 - vertical - 5 - - - True - False - 5 - 5 - Motion - - - - - - False - True - 0 - - - - - Enable Motion Controls - True - True - False - True - - - False - True - 1 - - - - - True - False - 10 - - - True - False - 17 - Controller Slot - - - False - True - 5 - 0 - - - - - True - True - 10 - _slotNumber - 1 - True - True - - - False - True - 1 - - - - - False - True - 5 - 2 - - - - - True - False - 10 - - - True - False - 5 - Gyro Sensitivity % - - - False - True - 5 - 0 - - - - - True - True - 0 - _sensitivity - 1 - True - True - - - False - True - 1 - - - - - False - True - 5 - 3 - - - - - True - False - vertical - - - Mirror Input - True - True - False - True - - - False - True - 0 - - - - - True - False - 10 - - - True - False - Right JoyCon Slot - - - False - True - 5 - 0 - - - - - True - True - 0 - _altSlotNumber - 1 - True - True - - - False - True - 1 - - - - - False - True - 5 - 1 - - - - - False - True - 4 - - - - - True - False - 30 - - - True - False - Server Host - - - False - True - 5 - 0 - - - - - True - True - - - False - True - 1 - - - - - False - True - 5 - 5 - - - - - True - False - 30 - - - True - False - Server Port - - - False - True - 5 - 0 - - - - - True - True - - - False - True - 1 - - - - - False - True - 5 - 6 - - - - - True - False - start - Gyro Deadzone - - - False - True - 7 - - - - - True - True - _gyroDeadzone - 2 - 2 - - - True - True - 8 - - - - - False - True - 4 - - - - - False - True - 1 - - - - - True - True - 0 - - - - - True - False - 10 - 20 - 5 - 5 - - - True - True - 1 - - - - - True - True - 1 - - - - - - - - - True - True - 0 - - - - - True - False - 5 - 3 - 3 - end - - - Save - True - True - True - - - - False - True - 0 - - - - - Close - True - True - True - 4 - - - - False - True - 5 - 1 - - - - - False - False - 1 - - - - - - diff --git a/Ryujinx/Ui/Diagnostic/GuideDialog.cs b/Ryujinx/Ui/Diagnostic/GuideDialog.cs deleted file mode 100644 index c3a0dd38..00000000 --- a/Ryujinx/Ui/Diagnostic/GuideDialog.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Gtk; -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Text; - -namespace Ryujinx.Ui.Diagnostic -{ - internal class GuideDialog : MessageDialog - { - internal static bool _isExitDialogOpen = false; - - public GuideDialog(string title, string mainText, string secondaryText) : base(null, DialogFlags.Modal, MessageType.Other, ButtonsType.None, null) - { - Title = title; - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); - Text = mainText; - SecondaryText = secondaryText; - WindowPosition = WindowPosition.Center; - Response += GtkDialog_Response; - - Button guideButton = new Button(); - guideButton.Label = "Open the Setup Guide"; - - ContentArea.Add(guideButton); - - SetSizeRequest(100, 10); - ShowAll(); - } - - private void GtkDialog_Response(object sender, ResponseArgs args) - { - Dispose(); - } - } -} diff --git a/Ryujinx/Ui/Diagnostic/SetupValidator.cs b/Ryujinx/Ui/Diagnostic/SetupValidator.cs deleted file mode 100644 index c52dc2ef..00000000 --- a/Ryujinx/Ui/Diagnostic/SetupValidator.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Ryujinx.Common.Logging; -using Ryujinx.HLE.FileSystem.Content; -using System; -using System.IO; - -namespace Ryujinx.Ui.Diagnostic -{ - /// - /// Ensure installation validity - /// - static class SetupValidator - { - public static bool IsFirmwareValid(ContentManager contentManager, out UserError error) - { - bool hasFirmware = contentManager.GetCurrentFirmwareVersion() != null; - - if (hasFirmware) - { - error = UserError.Success; - - return true; - } - else - { - error = UserError.NoFirmware; - - return false; - } - } - - public static bool CanFixStartApplication(ContentManager contentManager, string baseApplicationPath, UserError error, out SystemVersion firmwareVersion) - { - try - { - firmwareVersion = contentManager.VerifyFirmwarePackage(baseApplicationPath); - } - catch (Exception) - { - firmwareVersion = null; - } - - return error == UserError.NoFirmware && Path.GetExtension(baseApplicationPath).ToLowerInvariant() == ".xci" && firmwareVersion != null; - } - - public static bool TryFixStartApplication(ContentManager contentManager, string baseApplicationPath, UserError error, out UserError outError) - { - if (error == UserError.NoFirmware) - { - string baseApplicationExtension = Path.GetExtension(baseApplicationPath).ToLowerInvariant(); - - // If the target app to start is a XCI, try to install firmware from it - if (baseApplicationExtension == ".xci") - { - SystemVersion firmwareVersion; - - try - { - firmwareVersion = contentManager.VerifyFirmwarePackage(baseApplicationPath); - } - catch (Exception) - { - firmwareVersion = null; - } - - // The XCI is a valid firmware package, try to install the firmware from it! - if (firmwareVersion != null) - { - try - { - Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); - - contentManager.InstallFirmware(baseApplicationPath); - - Logger.Info?.Print(LogClass.Application, $"System version {firmwareVersion.VersionString} successfully installed."); - - outError = UserError.Success; - - return true; - } - catch (Exception) { } - } - - outError = error; - - return false; - } - } - - outError = error; - - return false; - } - - public static bool CanStartApplication(ContentManager contentManager, string baseApplicationPath, out UserError error) - { - if (Directory.Exists(baseApplicationPath) || File.Exists(baseApplicationPath)) - { - string baseApplicationExtension = Path.GetExtension(baseApplicationPath).ToLowerInvariant(); - - // NOTE: We don't force homebrew developers to install a system firmware. - if (baseApplicationExtension == ".nro" || baseApplicationExtension == ".nso") - { - error = UserError.Success; - - return true; - } - - return IsFirmwareValid(contentManager, out error); - } - else - { - error = UserError.ApplicationNotFound; - - return false; - } - } - } -} diff --git a/Ryujinx/Ui/Diagnostic/UserError.cs b/Ryujinx/Ui/Diagnostic/UserError.cs deleted file mode 100644 index eaa1bc83..00000000 --- a/Ryujinx/Ui/Diagnostic/UserError.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Ryujinx.Ui.Diagnostic -{ - /// - /// Represent a common error that could be reported to the user by the emulator. - /// - public enum UserError - { - /// - /// No error to report. - /// - Success = 0x0, - - /// - /// No keys are present. - /// - NoKeys = 0x1, - - /// - /// No firmware is installed. - /// - NoFirmware = 0x2, - - /// - /// Firmware parsing failed. - /// - /// Most likely related to keys. - FirmwareParsingFailed = 0x3, - - /// - /// No application was found at the given path. - /// - ApplicationNotFound = 0x4, - - /// - /// An unknown error. - /// - Unknown = 0xDEAD - } -} diff --git a/Ryujinx/Ui/Diagnostic/UserErrorDialog.cs b/Ryujinx/Ui/Diagnostic/UserErrorDialog.cs deleted file mode 100644 index ccea9229..00000000 --- a/Ryujinx/Ui/Diagnostic/UserErrorDialog.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Gtk; -using System.Reflection; - -namespace Ryujinx.Ui.Diagnostic -{ - internal class UserErrorDialog : MessageDialog - { - private static string SetupGuideUrl = "https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide"; - private const int OkResponseId = 0; - private const int SetupGuideResponseId = 1; - - private UserError _userError; - - private UserErrorDialog(UserError error) : base(null, DialogFlags.Modal, MessageType.Error, ButtonsType.None, null) - { - _userError = error; - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); - WindowPosition = WindowPosition.Center; - Response += UserErrorDialog_Response; - - SetSizeRequest(120, 50); - - AddButton("OK", OkResponseId); - - bool isInSetupGuide = IsCoveredBySetupGuide(error); - - if (isInSetupGuide) - { - AddButton("Open the Setup Guide", SetupGuideResponseId); - } - - string errorCode = GetErrorCode(error); - - SecondaryUseMarkup = true; - - Title = $"Ryujinx error ({errorCode})"; - Text = $"{errorCode}: {GetErrorTitle(error)}"; - SecondaryText = GetErrorDescription(error); - - if (isInSetupGuide) - { - SecondaryText += "\nFor more information on how to fix this error, follow our Setup Guide."; - } - } - - private static string GetErrorCode(UserError error) - { - return $"RYU-{(uint)error:X4}"; - } - - private static string GetErrorTitle(UserError error) - { - switch (error) - { - case UserError.NoKeys: - return "Keys not found"; - case UserError.NoFirmware: - return "Firmware not found"; - case UserError.FirmwareParsingFailed: - return "Firmware parsing error"; - case UserError.ApplicationNotFound: - return "Application not found"; - case UserError.Unknown: - return "Unknown error"; - default: - return "Undefined error"; - } - } - - private static string GetErrorDescription(UserError error) - { - switch (error) - { - case UserError.NoKeys: - return "Ryujinx was unable to find your 'prod.keys' file"; - case UserError.NoFirmware: - return "Ryujinx was unable to find any firmwares installed"; - case UserError.FirmwareParsingFailed: - return "Ryujinx was unable to parse the provided firmware. This is usually caused by outdated keys."; - case UserError.ApplicationNotFound: - return "Ryujinx couldn't find a valid application at the given path."; - case UserError.Unknown: - return "An unknown error occured!"; - default: - return "An undefined error occured! This shouldn't happen, please contact a dev!"; - } - } - - private static bool IsCoveredBySetupGuide(UserError error) - { - switch (error) - { - case UserError.NoKeys: - case UserError.NoFirmware: - case UserError.FirmwareParsingFailed: - return true; - default: - return false; - } - } - - private static string GetSetupGuideUrl(UserError error) - { - if (!IsCoveredBySetupGuide(error)) - { - return null; - } - - switch (error) - { - case UserError.NoKeys: - return SetupGuideUrl + "#initial-setup---placement-of-prodkeys"; - case UserError.NoFirmware: - return SetupGuideUrl + "#initial-setup-continued---installation-of-firmware"; - } - - return SetupGuideUrl; - } - - private void UserErrorDialog_Response(object sender, ResponseArgs args) - { - int responseId = (int)args.ResponseId; - - if (responseId == SetupGuideResponseId) - { - UrlHelper.OpenUrl(GetSetupGuideUrl(_userError)); - } - - Dispose(); - } - - public static void CreateUserErrorDialog(UserError error) - { - new UserErrorDialog(error).Run(); - } - } -} diff --git a/Ryujinx/Ui/DlcWindow.cs b/Ryujinx/Ui/DlcWindow.cs deleted file mode 100644 index 29e96b07..00000000 --- a/Ryujinx/Ui/DlcWindow.cs +++ /dev/null @@ -1,265 +0,0 @@ -using Gtk; -using LibHac; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.FsSystem; -using LibHac.FsSystem.NcaUtils; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; -using Ryujinx.HLE.FileSystem; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -using GUI = Gtk.Builder.ObjectAttribute; -using JsonHelper = Ryujinx.Common.Utilities.JsonHelper; - -namespace Ryujinx.Ui -{ - public class DlcWindow : Window - { - private readonly VirtualFileSystem _virtualFileSystem; - private readonly string _titleId; - private readonly string _dlcJsonPath; - private readonly List _dlcContainerList; - -#pragma warning disable CS0649, IDE0044 - [GUI] Label _baseTitleInfoLabel; - [GUI] TreeView _dlcTreeView; - [GUI] TreeSelection _dlcTreeSelection; -#pragma warning restore CS0649, IDE0044 - - public DlcWindow(string titleId, string titleName, VirtualFileSystem virtualFileSystem) : this(new Builder("Ryujinx.Ui.DlcWindow.glade"), titleId, titleName, virtualFileSystem) { } - - private DlcWindow(Builder builder, string titleId, string titleName, VirtualFileSystem virtualFileSystem) : base(builder.GetObject("_dlcWindow").Handle) - { - builder.Autoconnect(this); - - _titleId = titleId; - _virtualFileSystem = virtualFileSystem; - _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json"); - _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]"; - - try - { - _dlcContainerList = JsonHelper.DeserializeFromFile>(_dlcJsonPath); - } - catch - { - _dlcContainerList = new List(); - } - - _dlcTreeView.Model = new TreeStore( - typeof(bool), - typeof(string), - typeof(string)); - - CellRendererToggle enableToggle = new CellRendererToggle(); - enableToggle.Toggled += (sender, args) => - { - _dlcTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path)); - bool newValue = !(bool)_dlcTreeView.Model.GetValue(treeIter, 0); - _dlcTreeView.Model.SetValue(treeIter, 0, newValue); - - if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, treeIter)) - { - do - { - _dlcTreeView.Model.SetValue(childIter, 0, newValue); - } - while (_dlcTreeView.Model.IterNext(ref childIter)); - } - }; - - _dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0); - _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1); - _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); - - foreach (DlcContainer dlcContainer in _dlcContainerList) - { - TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", dlcContainer.Path); - - using FileStream containerFile = File.OpenRead(dlcContainer.Path); - PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage()); - _virtualFileSystem.ImportTickets(pfs); - - foreach (DlcNca dlcNca in dlcContainer.DlcNcaList) - { - pfs.OpenFile(out IFile ncaFile, dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - Nca nca = TryCreateNca(ncaFile.AsStorage(), dlcContainer.Path); - - if (nca != null) - { - ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.Path); - } - } - } - } - - private Nca TryCreateNca(IStorage ncaStorage, string containerPath) - { - try - { - return new Nca(_virtualFileSystem.KeySet, ncaStorage); - } - catch (InvalidDataException exception) - { - Logger.Error?.Print(LogClass.Application, $"{exception.Message}. Errored File: {containerPath}"); - - GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add DLC Failed!", "The NCA header content type check has failed. This is usually because the header key is incorrect or missing."); - } - catch (MissingKeyException exception) - { - Logger.Error?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {containerPath}"); - - GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add DLC Failed!", $"Your key set is missing a key with the name: {exception.Name}"); - } - - return null; - } - - private void AddButton_Clicked(object sender, EventArgs args) - { - FileChooserDialog fileChooser = new FileChooserDialog("Select DLC files", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept) - { - SelectMultiple = true, - Filter = new FileFilter() - }; - fileChooser.SetPosition(WindowPosition.Center); - fileChooser.Filter.AddPattern("*.nsp"); - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - foreach (string containerPath in fileChooser.Filenames) - { - if (!File.Exists(containerPath)) - { - return; - } - - using (FileStream containerFile = File.OpenRead(containerPath)) - { - PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage()); - bool containsDlc = false; - - _virtualFileSystem.ImportTickets(pfs); - - TreeIter? parentIter = null; - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = TryCreateNca(ncaFile.AsStorage(), containerPath); - - if (nca == null) continue; - - if (nca.Header.ContentType == NcaContentType.PublicData) - { - if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId) - { - break; - } - - parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath); - - ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); - containsDlc = true; - } - } - - if (!containsDlc) - { - GtkDialog.CreateErrorDialog("The specified file does not contain a DLC for the selected title!"); - } - } - } - } - - fileChooser.Dispose(); - } - - private void RemoveButton_Clicked(object sender, EventArgs args) - { - if (_dlcTreeSelection.GetSelected(out ITreeModel treeModel, out TreeIter treeIter)) - { - if (_dlcTreeView.Model.IterParent(out TreeIter parentIter, treeIter) && _dlcTreeView.Model.IterNChildren(parentIter) <= 1) - { - ((TreeStore)treeModel).Remove(ref parentIter); - } - else - { - ((TreeStore)treeModel).Remove(ref treeIter); - } - } - } - - private void RemoveAllButton_Clicked(object sender, EventArgs args) - { - List toRemove = new List(); - - if (_dlcTreeView.Model.GetIterFirst(out TreeIter iter)) - { - do - { - toRemove.Add(iter); - } - while (_dlcTreeView.Model.IterNext(ref iter)); - } - - foreach (TreeIter i in toRemove) - { - TreeIter j = i; - ((TreeStore)_dlcTreeView.Model).Remove(ref j); - } - } - - private void SaveButton_Clicked(object sender, EventArgs args) - { - _dlcContainerList.Clear(); - - if (_dlcTreeView.Model.GetIterFirst(out TreeIter parentIter)) - { - do - { - if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter)) - { - DlcContainer dlcContainer = new DlcContainer - { - Path = (string)_dlcTreeView.Model.GetValue(parentIter, 2), - DlcNcaList = new List() - }; - - do - { - dlcContainer.DlcNcaList.Add(new DlcNca - { - Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0), - TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16), - Path = (string)_dlcTreeView.Model.GetValue(childIter, 2) - }); - } - while (_dlcTreeView.Model.IterNext(ref childIter)); - - _dlcContainerList.Add(dlcContainer); - } - } - while (_dlcTreeView.Model.IterNext(ref parentIter)); - } - - using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough)) - { - dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true))); - } - - Dispose(); - } - - private void CancelButton_Clicked(object sender, EventArgs args) - { - Dispose(); - } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/DlcWindow.glade b/Ryujinx/Ui/DlcWindow.glade deleted file mode 100644 index cd0d8674..00000000 --- a/Ryujinx/Ui/DlcWindow.glade +++ /dev/null @@ -1,202 +0,0 @@ - - - - - - False - Ryujinx - DLC Manager - True - center - 550 - 350 - - - True - False - vertical - - - True - False - vertical - - - True - False - 10 - 10 - 10 - 10 - Available DLC - - - False - True - 0 - - - - - True - True - 10 - 10 - in - - - True - False - - - True - True - False - - - - - - - - - - True - True - 1 - - - - - True - True - 0 - - - - - True - False - - - True - False - 10 - 10 - start - - - Add - True - True - True - Adds an update to this list - 10 - - - - True - True - 0 - - - - - Remove - True - True - True - Removes the selected update - 10 - - - - True - True - 1 - - - - - Remove All - True - True - True - Removes the selected update - 10 - - - - True - True - 2 - - - - - True - True - 0 - - - - - True - False - 10 - 10 - end - - - Save - True - True - True - 10 - 2 - 2 - - - - True - True - 0 - - - - - Cancel - True - True - True - 10 - 2 - 2 - - - - True - True - 1 - - - - - True - True - 1 - - - - - False - True - 1 - - - - - - - - - diff --git a/Ryujinx/Ui/ErrorAppletDialog.cs b/Ryujinx/Ui/ErrorAppletDialog.cs deleted file mode 100644 index f7a548b3..00000000 --- a/Ryujinx/Ui/ErrorAppletDialog.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Gtk; -using System.Reflection; - -namespace Ryujinx.Ui -{ - internal class ErrorAppletDialog : MessageDialog - { - internal static bool _isExitDialogOpen = false; - - public ErrorAppletDialog(Window parentWindow, DialogFlags dialogFlags, MessageType messageType, string[] buttons) : base(parentWindow, dialogFlags, messageType, ButtonsType.None, null) - { - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); - - int responseId = 0; - - if (buttons != null) - { - foreach (string buttonText in buttons) - { - AddButton(buttonText, responseId); - responseId++; - } - } - else - { - AddButton("OK", 0); - } - - ShowAll(); - } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/GLRenderer.cs b/Ryujinx/Ui/GLRenderer.cs index c20cc78b..6115c22b 100644 --- a/Ryujinx/Ui/GLRenderer.cs +++ b/Ryujinx/Ui/GLRenderer.cs @@ -11,7 +11,8 @@ using Ryujinx.Configuration; using Ryujinx.Graphics.OpenGL; using Ryujinx.HLE; using Ryujinx.HLE.HOS.Services.Hid; -using Ryujinx.Motion; +using Ryujinx.Modules.Motion; +using Ryujinx.Ui.Widgets; using System; using System.Collections.Generic; using System.Threading; @@ -73,9 +74,9 @@ namespace Ryujinx.Ui _device = device; - this.Initialized += GLRenderer_Initialized; - this.Destroyed += GLRenderer_Destroyed; - this.ShuttingDown += GLRenderer_ShuttingDown; + Initialized += GLRenderer_Initialized; + Destroyed += GLRenderer_Destroyed; + ShuttingDown += GLRenderer_ShuttingDown; Initialize(); @@ -89,7 +90,7 @@ namespace Ryujinx.Ui | EventMask.KeyPressMask | EventMask.KeyReleaseMask)); - this.Shown += Renderer_Shown; + Shown += Renderer_Shown; _dsuClient = new Client(); diff --git a/Ryujinx/Ui/GameTableContextMenu.cs b/Ryujinx/Ui/GameTableContextMenu.cs deleted file mode 100644 index 58c40791..00000000 --- a/Ryujinx/Ui/GameTableContextMenu.cs +++ /dev/null @@ -1,750 +0,0 @@ -using Gtk; -using LibHac; -using LibHac.Account; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.Fs.Shim; -using LibHac.FsSystem; -using LibHac.FsSystem.NcaUtils; -using LibHac.Ncm; -using LibHac.Ns; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; -using Ryujinx.Common.Utilities; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS; -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Threading; - -using static LibHac.Fs.ApplicationSaveDataManagement; - -namespace Ryujinx.Ui -{ - public class GameTableContextMenu : Menu - { - private readonly ListStore _gameTableStore; - private readonly TreeIter _rowIter; - private readonly VirtualFileSystem _virtualFileSystem; - - private readonly BlitStruct _controlData; - - private MessageDialog _dialog; - private bool _cancel; - - public GameTableContextMenu(ListStore gameTableStore, BlitStruct controlData, TreeIter rowIter, VirtualFileSystem virtualFileSystem) - { - _gameTableStore = gameTableStore; - _rowIter = rowIter; - _virtualFileSystem = virtualFileSystem; - _controlData = controlData; - - MenuItem openSaveUserDir = new MenuItem("Open User Save Directory") - { - Sensitive = !Utilities.IsEmpty(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0, - TooltipText = "Open the directory which contains Application's User Saves." - }; - - MenuItem openSaveDeviceDir = new MenuItem("Open Device Save Directory") - { - Sensitive = !Utilities.IsEmpty(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0, - TooltipText = "Open the directory which contains Application's Device Saves." - }; - - MenuItem openSaveBcatDir = new MenuItem("Open BCAT Save Directory") - { - Sensitive = !Utilities.IsEmpty(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0, - TooltipText = "Open the directory which contains Application's BCAT Saves." - }; - - MenuItem manageTitleUpdates = new MenuItem("Manage Title Updates") - { - TooltipText = "Open the Title Update management window" - }; - - MenuItem manageDlc = new MenuItem("Manage DLC") - { - TooltipText = "Open the DLC management window" - }; - - MenuItem openTitleModDir = new MenuItem("Open Mods Directory") - { - TooltipText = "Open the directory which contains Application's Mods." - }; - - string ext = System.IO.Path.GetExtension(_gameTableStore.GetValue(_rowIter, 9).ToString()).ToLower(); - bool hasNca = ext == ".nca" || ext == ".nsp" || ext == ".pfs0" || ext == ".xci"; - - MenuItem extractMenu = new MenuItem("Extract Data"); - - MenuItem extractRomFs = new MenuItem("RomFS") - { - Sensitive = hasNca, - TooltipText = "Extract the RomFS section from Application's current config (including updates)." - }; - - MenuItem extractExeFs = new MenuItem("ExeFS") - { - Sensitive = hasNca, - TooltipText = "Extract the ExeFS section from Application's current config (including updates)." - }; - - MenuItem extractLogo = new MenuItem("Logo") - { - Sensitive = hasNca, - TooltipText = "Extract the Logo section from Application's current config (including updates)." - }; - - Menu extractSubMenu = new Menu(); - - extractSubMenu.Append(extractExeFs); - extractSubMenu.Append(extractRomFs); - extractSubMenu.Append(extractLogo); - - extractMenu.Submenu = extractSubMenu; - - MenuItem managePtcMenu = new MenuItem("Cache Management"); - - MenuItem purgePtcCache = new MenuItem("Purge PPTC Cache") - { - TooltipText = "Delete the Application's PPTC cache." - }; - - MenuItem purgeShaderCache = new MenuItem("Purge Shader Cache") - { - TooltipText = "Delete the Application's shader cache." - }; - - MenuItem openPtcDir = new MenuItem("Open PPTC Directory") - { - TooltipText = "Open the directory which contains the Application's PPTC cache." - }; - - MenuItem openShaderCacheDir = new MenuItem("Open Shader Cache Directory") - { - TooltipText = "Open the directory which contains the Application's shader cache." - }; - - Menu manageSubMenu = new Menu(); - - manageSubMenu.Append(purgePtcCache); - manageSubMenu.Append(purgeShaderCache); - manageSubMenu.Append(openPtcDir); - manageSubMenu.Append(openShaderCacheDir); - - managePtcMenu.Submenu = manageSubMenu; - - openSaveUserDir.Activated += OpenSaveUserDir_Clicked; - openSaveDeviceDir.Activated += OpenSaveDeviceDir_Clicked; - openSaveBcatDir.Activated += OpenSaveBcatDir_Clicked; - manageTitleUpdates.Activated += ManageTitleUpdates_Clicked; - manageDlc.Activated += ManageDlc_Clicked; - openTitleModDir.Activated += OpenTitleModDir_Clicked; - extractRomFs.Activated += ExtractRomFs_Clicked; - extractExeFs.Activated += ExtractExeFs_Clicked; - extractLogo.Activated += ExtractLogo_Clicked; - purgePtcCache.Activated += PurgePtcCache_Clicked; - purgeShaderCache.Activated += PurgeShaderCache_Clicked; - openPtcDir.Activated += OpenPtcDir_Clicked; - openShaderCacheDir.Activated += OpenShaderCacheDir_Clicked; - - this.Add(openSaveUserDir); - this.Add(openSaveDeviceDir); - this.Add(openSaveBcatDir); - this.Add(new SeparatorMenuItem()); - this.Add(manageTitleUpdates); - this.Add(manageDlc); - this.Add(openTitleModDir); - this.Add(new SeparatorMenuItem()); - this.Add(managePtcMenu); - this.Add(extractMenu); - } - - private bool TryFindSaveData(string titleName, ulong titleId, BlitStruct controlHolder, SaveDataFilter filter, out ulong saveDataId) - { - saveDataId = default; - - Result result = _virtualFileSystem.FsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter); - - if (ResultFs.TargetNotFound.Includes(result)) - { - // Savedata was not found. Ask the user if they want to create it - using MessageDialog messageDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, null) - { - Title = "Ryujinx", - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"), - Text = $"There is no savedata for {titleName} [{titleId:x16}]", - SecondaryText = "Would you like to create savedata for this game?", - WindowPosition = WindowPosition.Center - }; - - if (messageDialog.Run() != (int)ResponseType.Yes) - { - return false; - } - - ref ApplicationControlProperty control = ref controlHolder.Value; - - if (LibHac.Utilities.IsEmpty(controlHolder.ByteSpan)) - { - // If the current application doesn't have a loaded control property, create a dummy one - // and set the savedata sizes so a user savedata will be created. - control = ref new BlitStruct(1).Value; - - // The set sizes don't actually matter as long as they're non-zero because we use directory savedata. - control.UserAccountSaveDataSize = 0x4000; - control.UserAccountSaveDataJournalSize = 0x4000; - - Logger.Warning?.Print(LogClass.Application, - "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games."); - } - - Uid user = new Uid(1, 0); - - result = EnsureApplicationSaveData(_virtualFileSystem.FsClient, out _, new LibHac.Ncm.ApplicationId(titleId), ref control, ref user); - - if (result.IsFailure()) - { - GtkDialog.CreateErrorDialog($"There was an error creating the specified savedata: {result.ToStringWithName()}"); - - return false; - } - - // Try to find the savedata again after creating it - result = _virtualFileSystem.FsClient.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, ref filter); - } - - if (result.IsSuccess()) - { - saveDataId = saveDataInfo.SaveDataId; - - return true; - } - - GtkDialog.CreateErrorDialog($"There was an error finding the specified savedata: {result.ToStringWithName()}"); - - return false; - } - - private string GetSaveDataDirectory(ulong saveDataId) - { - string saveRootPath = System.IO.Path.Combine(_virtualFileSystem.GetNandPath(), $"user/save/{saveDataId:x16}"); - - if (!Directory.Exists(saveRootPath)) - { - // Inconsistent state. Create the directory - Directory.CreateDirectory(saveRootPath); - } - - string committedPath = System.IO.Path.Combine(saveRootPath, "0"); - string workingPath = System.IO.Path.Combine(saveRootPath, "1"); - - // If the committed directory exists, that path will be loaded the next time the savedata is mounted - if (Directory.Exists(committedPath)) - { - return committedPath; - } - - // If the working directory exists and the committed directory doesn't, - // the working directory will be loaded the next time the savedata is mounted - if (!Directory.Exists(workingPath)) - { - Directory.CreateDirectory(workingPath); - } - - return workingPath; - } - - private void ExtractSection(NcaSectionType ncaSectionType, int programIndex = 0) - { - FileChooserDialog fileChooser = new FileChooserDialog("Choose the folder to extract into", null, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Extract", ResponseType.Accept); - fileChooser.SetPosition(WindowPosition.Center); - - int response = fileChooser.Run(); - string destination = fileChooser.Filename; - - fileChooser.Dispose(); - - if (response == (int)ResponseType.Accept) - { - Thread extractorThread = new Thread(() => - { - string sourceFile = _gameTableStore.GetValue(_rowIter, 9).ToString(); - - Gtk.Application.Invoke(delegate - { - _dialog = new MessageDialog(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Cancel, null) - { - Title = "Ryujinx - NCA Section Extractor", - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"), - SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(sourceFile)}...", - WindowPosition = WindowPosition.Center - }; - - int dialogResponse = _dialog.Run(); - if (dialogResponse == (int)ResponseType.Cancel || dialogResponse == (int)ResponseType.DeleteEvent) - { - _cancel = true; - _dialog.Dispose(); - } - }); - - using (FileStream file = new FileStream(sourceFile, FileMode.Open, FileAccess.Read)) - { - Nca mainNca = null; - Nca patchNca = null; - - if ((System.IO.Path.GetExtension(sourceFile).ToLower() == ".nsp") || - (System.IO.Path.GetExtension(sourceFile).ToLower() == ".pfs0") || - (System.IO.Path.GetExtension(sourceFile).ToLower() == ".xci")) - { - PartitionFileSystem pfs; - - if (System.IO.Path.GetExtension(sourceFile) == ".xci") - { - Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()); - - pfs = xci.OpenPartition(XciPartitionType.Secure); - } - else - { - pfs = new PartitionFileSystem(file.AsStorage()); - } - - foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) - { - pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); - - Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); - - if (nca.Header.ContentType == NcaContentType.Program) - { - int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); - - if (nca.Header.GetFsHeader(dataIndex).IsPatchSection()) - { - patchNca = nca; - } - else - { - mainNca = nca; - } - } - } - } - else if (System.IO.Path.GetExtension(sourceFile).ToLower() == ".nca") - { - mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); - } - - if (mainNca == null) - { - Logger.Error?.Print(LogClass.Application, "Extraction failed. The main NCA was not present in the selected file."); - - Gtk.Application.Invoke(delegate - { - GtkDialog.CreateErrorDialog("Extraction failed. The main NCA was not present in the selected file."); - }); - - return; - } - - - (Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); - - if (updatePatchNca != null) - { - patchNca = updatePatchNca; - } - - int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType); - - IFileSystem ncaFileSystem = patchNca != null ? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid) - : mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid); - - FileSystemClient fsClient = _virtualFileSystem.FsClient; - - string source = DateTime.Now.ToFileTime().ToString().Substring(10); - string output = DateTime.Now.ToFileTime().ToString().Substring(10); - - fsClient.Register(source.ToU8Span(), ncaFileSystem); - fsClient.Register(output.ToU8Span(), new LocalFileSystem(destination)); - - (Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/"); - - if (!canceled) - { - if (resultCode.Value.IsFailure()) - { - Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}"); - - Gtk.Application.Invoke(delegate - { - _dialog?.Dispose(); - - GtkDialog.CreateErrorDialog("Extraction failed. Read the log file for further information."); - }); - } - else if (resultCode.Value.IsSuccess()) - { - Gtk.Application.Invoke(delegate - { - _dialog?.Dispose(); - - MessageDialog dialog = new MessageDialog(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null) - { - Title = "Ryujinx - NCA Section Extractor", - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"), - SecondaryText = "Extraction has completed successfully.", - WindowPosition = WindowPosition.Center - }; - - dialog.Run(); - dialog.Dispose(); - }); - } - } - - fsClient.Unmount(source.ToU8Span()); - fsClient.Unmount(output.ToU8Span()); - } - }); - - extractorThread.Name = "GUI.NcaSectionExtractorThread"; - extractorThread.IsBackground = true; - extractorThread.Start(); - } - } - - private (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath) - { - Result rc = fs.OpenDirectory(out DirectoryHandle sourceHandle, sourcePath.ToU8Span(), OpenDirectoryMode.All); - if (rc.IsFailure()) return (rc, false); - - using (sourceHandle) - { - foreach (DirectoryEntryEx entry in fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default)) - { - if (_cancel) - { - return (null, true); - } - - string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); - string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); - - if (entry.Type == DirectoryEntryType.Directory) - { - fs.EnsureDirectoryExists(subDstPath); - - (Result? result, bool canceled) = CopyDirectory(fs, subSrcPath, subDstPath); - if (canceled || result.Value.IsFailure()) - { - return (result, canceled); - } - } - - if (entry.Type == DirectoryEntryType.File) - { - fs.CreateOrOverwriteFile(subDstPath, entry.Size); - - rc = CopyFile(fs, subSrcPath, subDstPath); - if (rc.IsFailure()) return (rc, false); - } - } - } - - return (Result.Success, false); - } - - public Result CopyFile(FileSystemClient fs, string sourcePath, string destPath) - { - Result rc = fs.OpenFile(out FileHandle sourceHandle, sourcePath.ToU8Span(), OpenMode.Read); - if (rc.IsFailure()) return rc; - - using (sourceHandle) - { - rc = fs.OpenFile(out FileHandle destHandle, destPath.ToU8Span(), OpenMode.Write | OpenMode.AllowAppend); - if (rc.IsFailure()) return rc; - - using (destHandle) - { - const int maxBufferSize = 1024 * 1024; - - rc = fs.GetFileSize(out long fileSize, sourceHandle); - if (rc.IsFailure()) return rc; - - int bufferSize = (int)Math.Min(maxBufferSize, fileSize); - - byte[] buffer = ArrayPool.Shared.Rent(bufferSize); - try - { - for (long offset = 0; offset < fileSize; offset += bufferSize) - { - int toRead = (int)Math.Min(fileSize - offset, bufferSize); - Span buf = buffer.AsSpan(0, toRead); - - rc = fs.ReadFile(out long _, sourceHandle, offset, buf); - if (rc.IsFailure()) return rc; - - rc = fs.WriteFile(destHandle, offset, buf); - if (rc.IsFailure()) return rc; - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } - - rc = fs.FlushFile(destHandle); - if (rc.IsFailure()) return rc; - } - } - - return Result.Success; - } - - // Events - private void OpenSaveUserDir_Clicked(object sender, EventArgs args) - { - string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0]; - string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); - - if (!ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - GtkDialog.CreateErrorDialog("UI error: The selected game did not have a valid title ID"); - - return; - } - - SaveDataFilter filter = new SaveDataFilter(); - filter.SetUserId(new UserId(1, 0)); - - OpenSaveDir(titleName, titleIdNumber, filter); - } - - private void OpenSaveDir(string titleName, ulong titleId, SaveDataFilter filter) - { - filter.SetProgramId(new ProgramId(titleId)); - - if (!TryFindSaveData(titleName, titleId, _controlData, filter, out ulong saveDataId)) - { - return; - } - - string saveDir = GetSaveDataDirectory(saveDataId); - - Process.Start(new ProcessStartInfo - { - FileName = saveDir, - UseShellExecute = true, - Verb = "open" - }); - } - - private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args) - { - string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0]; - string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); - - if (!ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - GtkDialog.CreateErrorDialog("UI error: The selected game did not have a valid title ID"); - - return; - } - - SaveDataFilter filter = new SaveDataFilter(); - filter.SetSaveDataType(SaveDataType.Device); - - OpenSaveDir(titleName, titleIdNumber, filter); - } - - private void OpenSaveBcatDir_Clicked(object sender, EventArgs args) - { - string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0]; - string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); - - if (!ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber)) - { - GtkDialog.CreateErrorDialog("UI error: The selected game did not have a valid title ID"); - - return; - } - - SaveDataFilter filter = new SaveDataFilter(); - filter.SetSaveDataType(SaveDataType.Bcat); - - OpenSaveDir(titleName, titleIdNumber, filter); - } - - private void ManageTitleUpdates_Clicked(object sender, EventArgs args) - { - string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0]; - string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); - - TitleUpdateWindow titleUpdateWindow = new TitleUpdateWindow(titleId, titleName, _virtualFileSystem); - titleUpdateWindow.Show(); - } - - private void ManageDlc_Clicked(object sender, EventArgs args) - { - string titleName = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[0]; - string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); - - DlcWindow dlcWindow = new DlcWindow(titleId, titleName, _virtualFileSystem); - dlcWindow.Show(); - } - - private void OpenTitleModDir_Clicked(object sender, EventArgs args) - { - string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); - - var modsBasePath = _virtualFileSystem.ModLoader.GetModsBasePath(); - var titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId); - - Process.Start(new ProcessStartInfo - { - FileName = titleModsPath, - UseShellExecute = true, - Verb = "open" - }); - } - - private void ExtractRomFs_Clicked(object sender, EventArgs args) - { - ExtractSection(NcaSectionType.Data); - } - - private void ExtractExeFs_Clicked(object sender, EventArgs args) - { - ExtractSection(NcaSectionType.Code); - } - - private void ExtractLogo_Clicked(object sender, EventArgs args) - { - ExtractSection(NcaSectionType.Logo); - } - - private void OpenPtcDir_Clicked(object sender, EventArgs args) - { - string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); - string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "cpu"); - - string mainPath = System.IO.Path.Combine(ptcDir, "0"); - string backupPath = System.IO.Path.Combine(ptcDir, "1"); - - if (!Directory.Exists(ptcDir)) - { - Directory.CreateDirectory(ptcDir); - Directory.CreateDirectory(mainPath); - Directory.CreateDirectory(backupPath); - } - - Process.Start(new ProcessStartInfo - { - FileName = ptcDir, - UseShellExecute = true, - Verb = "open" - }); - } - - private void OpenShaderCacheDir_Clicked(object sender, EventArgs args) - { - string titleId = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n")[1].ToLower(); - string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "shader"); - - if (!Directory.Exists(shaderCacheDir)) - { - Directory.CreateDirectory(shaderCacheDir); - } - - Process.Start(new ProcessStartInfo - { - FileName = shaderCacheDir, - UseShellExecute = true, - Verb = "open" - }); - } - - private void PurgePtcCache_Clicked(object sender, EventArgs args) - { - string[] tableEntry = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n"); - string titleId = tableEntry[1].ToLower(); - - DirectoryInfo mainDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "cpu", "0")); - DirectoryInfo backupDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "cpu", "1")); - - MessageDialog warningDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null) - { - Title = "Ryujinx - Warning", - Text = $"You are about to delete the PPTC cache for '{tableEntry[0]}'. Are you sure you want to proceed?", - WindowPosition = WindowPosition.Center - }; - - List cacheFiles = new List(); - - if (mainDir.Exists) { cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); } - if (backupDir.Exists) { cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); } - - if (cacheFiles.Count > 0 && warningDialog.Run() == (int)ResponseType.Yes) - { - foreach (FileInfo file in cacheFiles) - { - try - { - file.Delete(); - } - catch(Exception e) - { - Logger.Error?.Print(LogClass.Application, $"Error purging PPTC cache {file.Name}: {e}"); - } - } - } - - warningDialog.Dispose(); - } - - private void PurgeShaderCache_Clicked(object sender, EventArgs args) - { - string[] tableEntry = _gameTableStore.GetValue(_rowIter, 2).ToString().Split("\n"); - string titleId = tableEntry[1].ToLower(); - - DirectoryInfo shaderCacheDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "shader")); - - MessageDialog warningDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null) - { - Title = "Ryujinx - Warning", - Text = $"You are about to delete the shader cache for '{tableEntry[0]}'. Are you sure you want to proceed?", - WindowPosition = WindowPosition.Center - }; - - List cacheDirectory = new List(); - - if (shaderCacheDir.Exists) { cacheDirectory.AddRange(shaderCacheDir.EnumerateDirectories("*")); } - - if (cacheDirectory.Count > 0 && warningDialog.Run() == (int)ResponseType.Yes) - { - foreach (DirectoryInfo directory in cacheDirectory) - { - try - { - directory.Delete(true); - } - catch (Exception e) - { - Logger.Error?.Print(LogClass.Application, $"Error purging shader cache {directory.Name}: {e}"); - } - } - } - - warningDialog.Dispose(); - } - } -} diff --git a/Ryujinx/Ui/GtkDialog.cs b/Ryujinx/Ui/GtkDialog.cs deleted file mode 100644 index f86b7016..00000000 --- a/Ryujinx/Ui/GtkDialog.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Gtk; -using System.Reflection; - -namespace Ryujinx.Ui -{ - internal class GtkDialog : MessageDialog - { - private static bool _isChoiceDialogOpen; - - private GtkDialog(string title, string mainText, string secondaryText, MessageType messageType = MessageType.Other, ButtonsType buttonsType = ButtonsType.Ok) - : base(null, DialogFlags.Modal, messageType, buttonsType, null) - { - Title = title; - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); - Text = mainText; - SecondaryText = secondaryText; - WindowPosition = WindowPosition.Center; - Response += GtkDialog_Response; - - SetSizeRequest(100, 20); - } - - private void GtkDialog_Response(object sender, ResponseArgs args) - { - Dispose(); - } - - internal static void CreateInfoDialog(string title, string mainText, string secondaryText) - { - new GtkDialog(title, mainText, secondaryText, MessageType.Info).Run(); - } - - internal static void CreateWarningDialog(string mainText, string secondaryText) - { - new GtkDialog("Ryujinx - Warning", mainText, secondaryText, MessageType.Warning).Run(); - } - - internal static void CreateErrorDialog(string errorMessage) - { - new GtkDialog("Ryujinx - Error", "Ryujinx has encountered an error", errorMessage, MessageType.Error).Run(); - } - - internal static MessageDialog CreateConfirmationDialog(string mainText, string secondaryText = "") - { - return new GtkDialog("Ryujinx - Confirmation", mainText, secondaryText, MessageType.Question, ButtonsType.YesNo); - } - - internal static bool CreateChoiceDialog(string title, string mainText, string secondaryText) - { - if (_isChoiceDialogOpen) - return false; - - _isChoiceDialogOpen = true; - ResponseType response = (ResponseType)new GtkDialog(title, mainText, secondaryText, MessageType.Question, ButtonsType.YesNo).Run(); - _isChoiceDialogOpen = false; - - return response == ResponseType.Yes; - } - - internal static bool CreateExitDialog() - { - return CreateChoiceDialog("Ryujinx - Exit", "Are you sure you want to stop emulation?", "All unsaved data will be lost!"); - } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/GtkHostUiHandler.cs b/Ryujinx/Ui/GtkHostUiHandler.cs deleted file mode 100644 index 12ba81c4..00000000 --- a/Ryujinx/Ui/GtkHostUiHandler.cs +++ /dev/null @@ -1,183 +0,0 @@ -using Gtk; -using Ryujinx.Common.Logging; -using Ryujinx.HLE; -using Ryujinx.HLE.HOS.Applets; -using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types; -using System; -using System.Threading; - -namespace Ryujinx.Ui -{ - internal class GtkHostUiHandler : IHostUiHandler - { - private readonly Window _parent; - - public GtkHostUiHandler(Window parent) - { - _parent = parent; - } - - public bool DisplayMessageDialog(ControllerAppletUiArgs args) - { - string playerCount = args.PlayerCountMin == args.PlayerCountMax - ? $"exactly {args.PlayerCountMin}" - : $"{args.PlayerCountMin}-{args.PlayerCountMax}"; - - string message = - $"Application requests {playerCount} player(s) with:\n\n" - + $"TYPES: {args.SupportedStyles}\n\n" - + $"PLAYERS: {string.Join(", ", args.SupportedPlayers)}\n\n" - + (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : "") - + "Please reconfigure Input now and then press OK."; - - return DisplayMessageDialog("Controller Applet", message); - } - - public bool DisplayMessageDialog(string title, string message) - { - ManualResetEvent dialogCloseEvent = new ManualResetEvent(false); - bool okPressed = false; - - Application.Invoke(delegate - { - MessageDialog msgDialog = null; - try - { - msgDialog = new MessageDialog(_parent, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null) - { - Title = title, - Text = message, - UseMarkup = true - }; - - msgDialog.SetDefaultSize(400, 0); - - msgDialog.Response += (object o, ResponseArgs args) => - { - if (args.ResponseId == ResponseType.Ok) okPressed = true; - dialogCloseEvent.Set(); - msgDialog?.Dispose(); - }; - - msgDialog.Show(); - } - catch (Exception e) - { - Logger.Error?.Print(LogClass.Application, $"Error displaying Message Dialog: {e}"); - dialogCloseEvent.Set(); - } - }); - - dialogCloseEvent.WaitOne(); - - return okPressed; - } - - public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) - { - ManualResetEvent dialogCloseEvent = new ManualResetEvent(false); - bool okPressed = false; - bool error = false; - string inputText = args.InitialText ?? ""; - - Application.Invoke(delegate - { - try - { - var swkbdDialog = new InputDialog(_parent) - { - Title = "Software Keyboard", - Text = args.HeaderText, - SecondaryText = args.SubtitleText - }; - - swkbdDialog.InputEntry.Text = inputText; - swkbdDialog.InputEntry.PlaceholderText = args.GuideText; - swkbdDialog.OkButton.Label = args.SubmitText; - - swkbdDialog.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax); - - if (swkbdDialog.Run() == (int)ResponseType.Ok) - { - inputText = swkbdDialog.InputEntry.Text; - okPressed = true; - } - - swkbdDialog.Dispose(); - } - catch (Exception e) - { - error = true; - Logger.Error?.Print(LogClass.Application, $"Error displaying Software Keyboard: {e}"); - } - finally - { - dialogCloseEvent.Set(); - } - }); - - dialogCloseEvent.WaitOne(); - - userText = error ? null : inputText; - - return error || okPressed; - } - - public void ExecuteProgram(HLE.Switch device, ProgramSpecifyKind kind, ulong value) - { - device.UserChannelPersistence.ExecuteProgram(kind, value); - MainWindow.GlWidget?.Exit(); - } - - public bool DisplayErrorAppletDialog(string title, string message, string[] buttons) - { - ManualResetEvent dialogCloseEvent = new ManualResetEvent(false); - bool showDetails = false; - - Application.Invoke(delegate - { - try - { - ErrorAppletDialog msgDialog = new ErrorAppletDialog(_parent, DialogFlags.DestroyWithParent, MessageType.Error, buttons) - { - Title = title, - Text = message, - UseMarkup = true, - WindowPosition = WindowPosition.CenterAlways - }; - - msgDialog.SetDefaultSize(400, 0); - - msgDialog.Response += (object o, ResponseArgs args) => - { - if (buttons != null) - { - if (buttons.Length > 1) - { - if (args.ResponseId != (ResponseType)(buttons.Length - 1)) - { - showDetails = true; - } - } - } - - dialogCloseEvent.Set(); - msgDialog?.Dispose(); - }; - - msgDialog.Show(); - } - catch (Exception e) - { - Logger.Error?.Print(LogClass.Application, $"Error displaying ErrorApplet Dialog: {e}"); - - dialogCloseEvent.Set(); - } - }); - - dialogCloseEvent.WaitOne(); - - return showDetails; - } - } -} diff --git a/Ryujinx/Ui/Helper/OpenHelper.cs b/Ryujinx/Ui/Helper/OpenHelper.cs new file mode 100644 index 00000000..6ccf4de3 --- /dev/null +++ b/Ryujinx/Ui/Helper/OpenHelper.cs @@ -0,0 +1,39 @@ +using Ryujinx.Common.Logging; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Ryujinx.Ui.Helper +{ + static class OpenHelper + { + public static void OpenFolder(string path) + { + Process.Start(new ProcessStartInfo + { + FileName = path, + UseShellExecute = true, + Verb = "open" + }); + } + + public static void OpenUrl(string url) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Process.Start(new ProcessStartInfo("cmd", $"/c start {url.Replace("&", "^&")}")); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", url); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url); + } + else + { + Logger.Notice.Print(LogClass.Application, $"Cannot open url \"{url}\" on this platform!"); + } + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Helper/SetupValidator.cs b/Ryujinx/Ui/Helper/SetupValidator.cs new file mode 100644 index 00000000..45315f8f --- /dev/null +++ b/Ryujinx/Ui/Helper/SetupValidator.cs @@ -0,0 +1,119 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem.Content; +using Ryujinx.Ui.Widgets; +using System; +using System.IO; + +namespace Ryujinx.Ui.Helper +{ + /// + /// Ensure installation validity + /// + static class SetupValidator + { + public static bool IsFirmwareValid(ContentManager contentManager, out UserError error) + { + bool hasFirmware = contentManager.GetCurrentFirmwareVersion() != null; + + if (hasFirmware) + { + error = UserError.Success; + + return true; + } + else + { + error = UserError.NoFirmware; + + return false; + } + } + + public static bool CanFixStartApplication(ContentManager contentManager, string baseApplicationPath, UserError error, out SystemVersion firmwareVersion) + { + try + { + firmwareVersion = contentManager.VerifyFirmwarePackage(baseApplicationPath); + } + catch (Exception) + { + firmwareVersion = null; + } + + return error == UserError.NoFirmware && Path.GetExtension(baseApplicationPath).ToLowerInvariant() == ".xci" && firmwareVersion != null; + } + + public static bool TryFixStartApplication(ContentManager contentManager, string baseApplicationPath, UserError error, out UserError outError) + { + if (error == UserError.NoFirmware) + { + string baseApplicationExtension = Path.GetExtension(baseApplicationPath).ToLowerInvariant(); + + // If the target app to start is a XCI, try to install firmware from it + if (baseApplicationExtension == ".xci") + { + SystemVersion firmwareVersion; + + try + { + firmwareVersion = contentManager.VerifyFirmwarePackage(baseApplicationPath); + } + catch (Exception) + { + firmwareVersion = null; + } + + // The XCI is a valid firmware package, try to install the firmware from it! + if (firmwareVersion != null) + { + try + { + Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); + + contentManager.InstallFirmware(baseApplicationPath); + + Logger.Info?.Print(LogClass.Application, $"System version {firmwareVersion.VersionString} successfully installed."); + + outError = UserError.Success; + + return true; + } + catch (Exception) { } + } + + outError = error; + + return false; + } + } + + outError = error; + + return false; + } + + public static bool CanStartApplication(ContentManager contentManager, string baseApplicationPath, out UserError error) + { + if (Directory.Exists(baseApplicationPath) || File.Exists(baseApplicationPath)) + { + string baseApplicationExtension = Path.GetExtension(baseApplicationPath).ToLowerInvariant(); + + // NOTE: We don't force homebrew developers to install a system firmware. + if (baseApplicationExtension == ".nro" || baseApplicationExtension == ".nso") + { + error = UserError.Success; + + return true; + } + + return IsFirmwareValid(contentManager, out error); + } + else + { + error = UserError.ApplicationNotFound; + + return false; + } + } + } +} diff --git a/Ryujinx/Ui/Helper/SortHelper.cs b/Ryujinx/Ui/Helper/SortHelper.cs new file mode 100644 index 00000000..78917fe7 --- /dev/null +++ b/Ryujinx/Ui/Helper/SortHelper.cs @@ -0,0 +1,116 @@ +using Gtk; +using System; + +namespace Ryujinx.Ui.Helper +{ + static class SortHelper + { + public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b) + { + string aValue = model.GetValue(a, 5).ToString(); + string bValue = model.GetValue(b, 5).ToString(); + + if (aValue.Length > 4 && aValue[^4..] == "mins") + { + aValue = (float.Parse(aValue[0..^5]) * 60).ToString(); + } + else if (aValue.Length > 3 && aValue[^3..] == "hrs") + { + aValue = (float.Parse(aValue[0..^4]) * 3600).ToString(); + } + else if (aValue.Length > 4 && aValue[^4..] == "days") + { + aValue = (float.Parse(aValue[0..^5]) * 86400).ToString(); + } + else + { + aValue = aValue[0..^1]; + } + + if (bValue.Length > 4 && bValue[^4..] == "mins") + { + bValue = (float.Parse(bValue[0..^5]) * 60).ToString(); + } + else if (bValue.Length > 3 && bValue[^3..] == "hrs") + { + bValue = (float.Parse(bValue[0..^4]) * 3600).ToString(); + } + else if (bValue.Length > 4 && bValue[^4..] == "days") + { + bValue = (float.Parse(bValue[0..^5]) * 86400).ToString(); + } + else + { + bValue = bValue[0..^1]; + } + + if (float.Parse(aValue) > float.Parse(bValue)) + { + return -1; + } + else if (float.Parse(bValue) > float.Parse(aValue)) + { + return 1; + } + else + { + return 0; + } + } + + public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b) + { + string aValue = model.GetValue(a, 6).ToString(); + string bValue = model.GetValue(b, 6).ToString(); + + if (aValue == "Never") + { + aValue = DateTime.UnixEpoch.ToString(); + } + + if (bValue == "Never") + { + bValue = DateTime.UnixEpoch.ToString(); + } + + return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue)); + } + + public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b) + { + string aValue = model.GetValue(a, 8).ToString(); + string bValue = model.GetValue(b, 8).ToString(); + + if (aValue[^2..] == "GB") + { + aValue = (float.Parse(aValue[0..^2]) * 1024).ToString(); + } + else + { + aValue = aValue[0..^2]; + } + + if (bValue[^2..] == "GB") + { + bValue = (float.Parse(bValue[0..^2]) * 1024).ToString(); + } + else + { + bValue = bValue[0..^2]; + } + + if (float.Parse(aValue) > float.Parse(bValue)) + { + return -1; + } + else if (float.Parse(bValue) > float.Parse(aValue)) + { + return 1; + } + else + { + return 0; + } + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Helper/ThemeHelper.cs b/Ryujinx/Ui/Helper/ThemeHelper.cs new file mode 100644 index 00000000..fca713e6 --- /dev/null +++ b/Ryujinx/Ui/Helper/ThemeHelper.cs @@ -0,0 +1,35 @@ +using Gtk; +using Ryujinx.Common.Logging; +using Ryujinx.Configuration; +using System.IO; + +namespace Ryujinx.Ui.Helper +{ + static class ThemeHelper + { + public static void ApplyTheme() + { + if (!ConfigurationState.Instance.Ui.EnableCustomTheme) + { + return; + } + + if (File.Exists(ConfigurationState.Instance.Ui.CustomThemePath) && (Path.GetExtension(ConfigurationState.Instance.Ui.CustomThemePath) == ".css")) + { + CssProvider cssProvider = new CssProvider(); + + cssProvider.LoadFromPath(ConfigurationState.Instance.Ui.CustomThemePath); + + StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800); + } + else + { + Logger.Warning?.Print(LogClass.Application, $"The \"custom_theme_path\" section in \"Config.json\" contains an invalid path: \"{ConfigurationState.Instance.Ui.CustomThemePath}\"."); + + ConfigurationState.Instance.Ui.CustomThemePath.Value = ""; + ConfigurationState.Instance.Ui.EnableCustomTheme.Value = false; + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/InputDialog.cs b/Ryujinx/Ui/InputDialog.cs deleted file mode 100644 index a8dc80bf..00000000 --- a/Ryujinx/Ui/InputDialog.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Gtk; -using System; - -namespace Ryujinx.Ui -{ - public class InputDialog : MessageDialog - { - private int _inputMin, _inputMax; - private Predicate _checkLength; - private Label _validationInfo; - - public Entry InputEntry { get; } - public Button OkButton { get; } - public Button CancelButton { get; } - - public InputDialog(Window parent) - : base(parent, DialogFlags.Modal | DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.None, null) - { - SetDefaultSize(300, 0); - - _validationInfo = new Label() { Visible = false }; - - InputEntry = new Entry() { Visible = true }; - InputEntry.Activated += (object sender, EventArgs e) => { if (OkButton.IsSensitive) Respond(ResponseType.Ok); }; - InputEntry.Changed += OnInputChanged; - - OkButton = (Button)AddButton("OK", ResponseType.Ok); - CancelButton = (Button)AddButton("Cancel", ResponseType.Cancel); - - ((Box)MessageArea).PackEnd(_validationInfo, true, true, 0); - ((Box)MessageArea).PackEnd(InputEntry, true, true, 4); - - SetInputLengthValidation(0, int.MaxValue); // disable by default - } - - public void SetInputLengthValidation(int min, int max) - { - _inputMin = Math.Min(min, max); - _inputMax = Math.Max(min, max); - - _validationInfo.Visible = false; - - if (_inputMin <= 0 && _inputMax == int.MaxValue) // disable - { - _validationInfo.Visible = false; - _checkLength = (length) => true; - } - else if (_inputMin > 0 && _inputMax == int.MaxValue) - { - _validationInfo.Visible = true; - _validationInfo.Markup = $"Must be at least {_inputMin} characters long"; - _checkLength = (length) => _inputMin <= length; - } - else - { - _validationInfo.Visible = true; - _validationInfo.Markup = $"Must be {_inputMin}-{_inputMax} characters long"; - _checkLength = (length) => _inputMin <= length && length <= _inputMax; - } - - OnInputChanged(this, EventArgs.Empty); - } - - private void OnInputChanged(object sender, EventArgs e) - { - OkButton.Sensitive = _checkLength(InputEntry.Text.Length); - } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index f16158e2..58cd6401 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -13,11 +13,16 @@ using Ryujinx.Graphics.OpenGL; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.HOS; -using Ryujinx.Ui.Diagnostic; +using Ryujinx.Modules; +using Ryujinx.Ui.App; +using Ryujinx.Ui.Applet; +using Ryujinx.Ui.Helper; +using Ryujinx.Ui.Widgets; +using Ryujinx.Ui.Windows; using System; using System.Diagnostics; using System.IO; -using System.Reflection; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -27,26 +32,27 @@ namespace Ryujinx.Ui { public class MainWindow : Window { - private static VirtualFileSystem _virtualFileSystem; - private static ContentManager _contentManager; - private static UserChannelPersistence _userChannelPersistence; + private readonly VirtualFileSystem _virtualFileSystem; + private readonly ContentManager _contentManager; - private static WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution; - private static HLE.Switch _emulationContext; + private UserChannelPersistence _userChannelPersistence; - private static GlRenderer _glWidget; - private static GtkHostUiHandler _uiHandler; + private HLE.Switch _emulationContext; - public static GlRenderer GlWidget => _glWidget; + private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution; - private static AutoResetEvent _deviceExitStatus = new AutoResetEvent(false); + private readonly ApplicationLibrary _applicationLibrary; + private readonly GtkHostUiHandler _uiHandler; + private readonly AutoResetEvent _deviceExitStatus; + private readonly ListStore _tableStore; - private static ListStore _tableStore; + private bool _updatingGameTable; + private bool _gameLoaded; + private bool _ending; - private static bool _updatingGameTable; - private static bool _gameLoaded; - private static string _gamePath; - private static bool _ending; + private string _currentEmulatedGamePath = null; + + public GlRenderer GlRendererWidget; #pragma warning disable CS0169, CS0649, IDE0044 @@ -93,50 +99,41 @@ namespace Ryujinx.Ui { builder.Autoconnect(this); + // Apply custom theme if needed. + ThemeHelper.ApplyTheme(); + + // Sets overridden fields. int monitorWidth = Display.PrimaryMonitor.Geometry.Width * Display.PrimaryMonitor.ScaleFactor; int monitorHeight = Display.PrimaryMonitor.Geometry.Height * Display.PrimaryMonitor.ScaleFactor; - this.DefaultWidth = monitorWidth < 1280 ? monitorWidth : 1280; - this.DefaultHeight = monitorHeight < 760 ? monitorHeight : 760; - - this.WindowStateEvent += MainWindow_WindowStateEvent; - this.DeleteEvent += Window_Close; - _fullScreen.Activated += FullScreen_Toggled; - - this.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); - this.Title = $"Ryujinx {Program.Version}"; - - ApplicationLibrary.ApplicationAdded += Application_Added; - ApplicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated; - GlRenderer.StatusUpdatedEvent += Update_StatusBar; + DefaultWidth = monitorWidth < 1280 ? monitorWidth : 1280; + DefaultHeight = monitorHeight < 760 ? monitorHeight : 760; - _gameTable.ButtonReleaseEvent += Row_Clicked; + Title = $"Ryujinx {Program.Version}"; - // First we check that a migration isn't needed. (because VirtualFileSystem will create the new directory otherwise) - bool continueWithStartup = Migration.PromptIfMigrationNeededForStartup(this, out bool migrationNeeded); - if (!continueWithStartup) - { - End(null); - } + // Hide emulation context status bar. + _statusBar.Hide(); + // Instanciate HLE objects. _virtualFileSystem = VirtualFileSystem.CreateInstance(); - _userChannelPersistence = new UserChannelPersistence(); _contentManager = new ContentManager(_virtualFileSystem); + _userChannelPersistence = new UserChannelPersistence(); - if (migrationNeeded) - { - bool migrationSuccessful = Migration.DoMigrationForStartup(this, _virtualFileSystem); + // Instanciate GUI objects. + _applicationLibrary = new ApplicationLibrary(_virtualFileSystem); + _uiHandler = new GtkHostUiHandler(this); + _deviceExitStatus = new AutoResetEvent(false); - if (!migrationSuccessful) - { - End(null); - } - } + WindowStateEvent += WindowStateEvent_Changed; + DeleteEvent += Window_Close; - // Make sure that everything is loaded. - _virtualFileSystem.Reload(); + _applicationLibrary.ApplicationAdded += Application_Added; + _applicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated; + + _gameTable.ButtonReleaseEvent += Row_Clicked; + _fullScreen.Activated += FullScreen_Toggled; - ApplyTheme(); + GlRenderer.StatusUpdatedEvent += Update_StatusBar; if (ConfigurationState.Instance.Ui.StartFullscreen) { @@ -169,9 +166,9 @@ namespace Ryujinx.Ui typeof(string), typeof(BlitStruct)); - _tableStore.SetSortFunc(5, TimePlayedSort); - _tableStore.SetSortFunc(6, LastPlayedSort); - _tableStore.SetSortFunc(8, FileSizeSort); + _tableStore.SetSortFunc(5, SortHelper.TimePlayedSort); + _tableStore.SetSortFunc(6, SortHelper.LastPlayedSort); + _tableStore.SetSortFunc(8, SortHelper.FileSizeSort); int columnId = ConfigurationState.Instance.Ui.ColumnSort.SortColumnId; bool ascending = ConfigurationState.Instance.Ui.ColumnSort.SortAscending; @@ -193,39 +190,13 @@ namespace Ryujinx.Ui }; Task.Run(RefreshFirmwareLabel); - - _statusBar.Hide(); - - _uiHandler = new GtkHostUiHandler(this); - _gamePath = null; } - private void MainWindow_WindowStateEvent(object o, WindowStateEventArgs args) + private void WindowStateEvent_Changed(object o, WindowStateEventArgs args) { _fullScreen.Label = args.Event.NewWindowState.HasFlag(Gdk.WindowState.Fullscreen) ? "Exit Fullscreen" : "Enter Fullscreen"; } - internal static void ApplyTheme() - { - if (!ConfigurationState.Instance.Ui.EnableCustomTheme) - { - return; - } - - if (File.Exists(ConfigurationState.Instance.Ui.CustomThemePath) && (System.IO.Path.GetExtension(ConfigurationState.Instance.Ui.CustomThemePath) == ".css")) - { - CssProvider cssProvider = new CssProvider(); - - cssProvider.LoadFromPath(ConfigurationState.Instance.Ui.CustomThemePath); - - StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800); - } - else - { - Logger.Warning?.Print(LogClass.Application, $"The \"custom_theme_path\" section in \"Config.json\" contains an invalid path: \"{ConfigurationState.Instance.Ui.CustomThemePath}\"."); - } - } - private void UpdateColumns() { foreach (TreeViewColumn column in _gameTable.Columns) @@ -291,21 +262,59 @@ namespace Ryujinx.Ui } } - private HLE.Switch InitializeSwitchInstance() + private void InitializeSwitchInstance() { _virtualFileSystem.Reload(); - HLE.Switch instance = new HLE.Switch(_virtualFileSystem, _contentManager, _userChannelPersistence, InitializeRenderer(), InitializeAudioEngine()) + IRenderer renderer = new Renderer(); + IAalOutput audioEngine = new DummyAudioOut(); + + if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SoundIo) + { + if (SoundIoAudioOut.IsSupported) + { + audioEngine = new SoundIoAudioOut(); + } + else + { + Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, falling back to dummy audio out."); + } + } + else if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.OpenAl) + { + if (OpenALAudioOut.IsSupported) + { + audioEngine = new OpenALAudioOut(); + } + else + { + Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, trying to fall back to SoundIO."); + + if (SoundIoAudioOut.IsSupported) + { + Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration."); + + ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo; + SaveConfig(); + + audioEngine = new SoundIoAudioOut(); + } + else + { + Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, falling back to dummy audio out."); + } + } + } + + _emulationContext = new HLE.Switch(_virtualFileSystem, _contentManager, _userChannelPersistence, renderer, audioEngine) { UiHandler = _uiHandler }; - instance.Initialize(); - - return instance; + _emulationContext.Initialize(); } - internal static void UpdateGameTable() + public void UpdateGameTable() { if (_updatingGameTable || _gameLoaded) { @@ -318,64 +327,68 @@ namespace Ryujinx.Ui Thread applicationLibraryThread = new Thread(() => { - ApplicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs, - _virtualFileSystem, ConfigurationState.Instance.System.Language); + _applicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs, ConfigurationState.Instance.System.Language); _updatingGameTable = false; }); - applicationLibraryThread.Name = "GUI.ApplicationLibraryThread"; + applicationLibraryThread.Name = "GUI.ApplicationLibraryThread"; applicationLibraryThread.IsBackground = true; applicationLibraryThread.Start(); } - internal void LoadApplication(string path) + [Conditional("RELEASE")] + public void PerformanceCheck() { - if (_gameLoaded) + if (ConfigurationState.Instance.Logger.EnableDebug.Value) { - GtkDialog.CreateInfoDialog("Ryujinx", "A game has already been loaded", "Please close it first and try again."); + MessageDialog debugWarningDialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null) + { + Title = "Ryujinx - Warning", + Text = "You have debug logging enabled, which is designed to be used by developers only.", + SecondaryText = "For optimal performance, it's recommended to disable debug logging. Would you like to disable debug logging now?" + }; + + if (debugWarningDialog.Run() == (int)ResponseType.Yes) + { + ConfigurationState.Instance.Logger.EnableDebug.Value = false; + SaveConfig(); + } + + debugWarningDialog.Dispose(); } - else + + if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value)) { -#if !DEBUG - if (ConfigurationState.Instance.Logger.EnableDebug.Value) + MessageDialog shadersDumpWarningDialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null) { - MessageDialog debugWarningDialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null) - { - Title = "Ryujinx - Warning", - Text = "You have debug logging enabled, which is designed to be used by developers only.", - SecondaryText = "For optimal performance, it's recommended to disable debug logging. Would you like to disable debug logging now?" - }; - - if (debugWarningDialog.Run() == (int)ResponseType.Yes) - { - ConfigurationState.Instance.Logger.EnableDebug.Value = false; - SaveConfig(); - } + Title = "Ryujinx - Warning", + Text = "You have shader dumping enabled, which is designed to be used by developers only.", + SecondaryText = "For optimal performance, it's recommended to disable shader dumping. Would you like to disable shader dumping now?" + }; - debugWarningDialog.Dispose(); + if (shadersDumpWarningDialog.Run() == (int)ResponseType.Yes) + { + ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = ""; + SaveConfig(); } - if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value)) - { - MessageDialog shadersDumpWarningDialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null) - { - Title = "Ryujinx - Warning", - Text = "You have shader dumping enabled, which is designed to be used by developers only.", - SecondaryText = "For optimal performance, it's recommended to disable shader dumping. Would you like to disable shader dumping now?" - }; + shadersDumpWarningDialog.Dispose(); + } + } - if (shadersDumpWarningDialog.Run() == (int)ResponseType.Yes) - { - ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = ""; - SaveConfig(); - } + public void LoadApplication(string path) + { + if (_gameLoaded) + { + GtkDialog.CreateInfoDialog("A game has already been loaded", "Please close it first and try again."); + } + else + { + PerformanceCheck(); - shadersDumpWarningDialog.Dispose(); - } -#endif Logger.RestartTime(); - HLE.Switch device = InitializeSwitchInstance(); + InitializeSwitchInstance(); UpdateGraphicsConfig(); @@ -389,34 +402,25 @@ namespace Ryujinx.Ui { if (userError == UserError.NoFirmware) { - MessageDialog shouldInstallFirmwareDialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.YesNo, null) - { - Title = "Ryujinx - Info", - Text = "No Firmware Installed", - SecondaryText = $"Would you like to install the firmware embedded in this game? (Firmware {firmwareVersion.VersionString})" - }; + string message = $"Would you like to install the firmware embedded in this game? (Firmware {firmwareVersion.VersionString})"; - if (shouldInstallFirmwareDialog.Run() != (int)ResponseType.Yes) - { - shouldInstallFirmwareDialog.Dispose(); + ResponseType responseDialog = (ResponseType)GtkDialog.CreateConfirmationDialog("No Firmware Installed", message).Run(); + if (responseDialog != ResponseType.Yes) + { UserErrorDialog.CreateUserErrorDialog(userError); - device.Dispose(); + _emulationContext.Dispose(); return; } - else - { - shouldInstallFirmwareDialog.Dispose(); - } } if (!SetupValidator.TryFixStartApplication(_contentManager, path, userError, out _)) { UserErrorDialog.CreateUserErrorDialog(userError); - device.Dispose(); + _emulationContext.Dispose(); return; } @@ -428,15 +432,16 @@ namespace Ryujinx.Ui RefreshFirmwareLabel(); - GtkDialog.CreateInfoDialog("Ryujinx - Info", $"Firmware {firmwareVersion.VersionString} was installed", - $"No installed firmware was found but Ryujinx was able to install firmware {firmwareVersion.VersionString} from the provided game.\nThe emulator will now start."); + string message = $"No installed firmware was found but Ryujinx was able to install firmware {firmwareVersion.VersionString} from the provided game.\nThe emulator will now start."; + + GtkDialog.CreateInfoDialog($"Firmware {firmwareVersion.VersionString} was installed", message); } } else { UserErrorDialog.CreateUserErrorDialog(userError); - device.Dispose(); + _emulationContext.Dispose(); return; } @@ -456,12 +461,12 @@ namespace Ryujinx.Ui if (romFsFiles.Length > 0) { Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS."); - device.LoadCart(path, romFsFiles[0]); + _emulationContext.LoadCart(path, romFsFiles[0]); } else { Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS."); - device.LoadCart(path); + _emulationContext.LoadCart(path); } } else if (File.Exists(path)) @@ -470,22 +475,22 @@ namespace Ryujinx.Ui { case ".xci": Logger.Info?.Print(LogClass.Application, "Loading as XCI."); - device.LoadXci(path); + _emulationContext.LoadXci(path); break; case ".nca": Logger.Info?.Print(LogClass.Application, "Loading as NCA."); - device.LoadNca(path); + _emulationContext.LoadNca(path); break; case ".nsp": case ".pfs0": Logger.Info?.Print(LogClass.Application, "Loading as NSP."); - device.LoadNsp(path); + _emulationContext.LoadNsp(path); break; default: Logger.Info?.Print(LogClass.Application, "Loading as homebrew."); try { - device.LoadProgram(path); + _emulationContext.LoadProgram(path); } catch (ArgumentOutOfRangeException) { @@ -497,23 +502,23 @@ namespace Ryujinx.Ui else { Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file."); - device.Dispose(); + + _emulationContext.Dispose(); return; } - _emulationContext = device; - _gamePath = path; + _currentEmulatedGamePath = path; _deviceExitStatus.Reset(); Translator.IsReadyForTranslation.Reset(); #if MACOS_BUILD - CreateGameWindow(device); + CreateGameWindow(); #else Thread windowThread = new Thread(() => { - CreateGameWindow(device); + CreateGameWindow(); }) { Name = "GUI.WindowThread" @@ -528,34 +533,34 @@ namespace Ryujinx.Ui _firmwareInstallFile.Sensitive = false; _firmwareInstallDirectory.Sensitive = false; - DiscordIntegrationModule.SwitchToPlayingState(device.Application.TitleIdText, device.Application.TitleName); + DiscordIntegrationModule.SwitchToPlayingState(_emulationContext.Application.TitleIdText, _emulationContext.Application.TitleName); - ApplicationLibrary.LoadAndSaveMetaData(device.Application.TitleIdText, appMetadata => + _applicationLibrary.LoadAndSaveMetaData(_emulationContext.Application.TitleIdText, appMetadata => { appMetadata.LastPlayed = DateTime.UtcNow.ToString(); }); } } - private void CreateGameWindow(HLE.Switch device) + private void CreateGameWindow() { - if (Environment.OSVersion.Platform == PlatformID.Win32NT) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1); } - _glWidget = new GlRenderer(_emulationContext, ConfigurationState.Instance.Logger.GraphicsDebugLevel); + GlRendererWidget = new GlRenderer(_emulationContext, ConfigurationState.Instance.Logger.GraphicsDebugLevel); Application.Invoke(delegate { _viewBox.Remove(_gameTableWindow); - _glWidget.Expand = true; - _viewBox.Child = _glWidget; + GlRendererWidget.Expand = true; + _viewBox.Child = GlRendererWidget; - _glWidget.ShowAll(); - EditFooterForGameRender(); + GlRendererWidget.ShowAll(); + EditFooterForGameRenderer(); - if (this.Window.State.HasFlag(Gdk.WindowState.Fullscreen)) + if (Window.State.HasFlag(Gdk.WindowState.Fullscreen)) { ToggleExtraWidgets(false); } @@ -565,33 +570,34 @@ namespace Ryujinx.Ui } }); - _glWidget.WaitEvent.WaitOne(); + GlRendererWidget.WaitEvent.WaitOne(); - _glWidget.Start(); + GlRendererWidget.Start(); Ptc.Close(); PtcProfiler.Stop(); - device.Dispose(); + _emulationContext.Dispose(); _deviceExitStatus.Set(); // NOTE: Everything that is here will not be executed when you close the UI. Application.Invoke(delegate { - if (this.Window.State.HasFlag(Gdk.WindowState.Fullscreen)) + if (Window.State.HasFlag(Gdk.WindowState.Fullscreen)) { ToggleExtraWidgets(true); } - _viewBox.Remove(_glWidget); - _glWidget.Exit(); + _viewBox.Remove(GlRendererWidget); + GlRendererWidget.Exit(); - if(_glWidget.Window != this.Window && _glWidget.Window != null) + if(GlRendererWidget.Window != Window && GlRendererWidget.Window != null) { - _glWidget.Window.Dispose(); + GlRendererWidget.Window.Dispose(); } - _glWidget.Dispose(); + GlRendererWidget.Dispose(); + _windowsMultimediaTimerResolution?.Dispose(); _windowsMultimediaTimerResolution = null; @@ -599,11 +605,11 @@ namespace Ryujinx.Ui _gameTableWindow.Expand = true; - this.Window.Title = $"Ryujinx {Program.Version}"; + Window.Title = $"Ryujinx {Program.Version}"; _emulationContext = null; _gameLoaded = false; - _glWidget = null; + GlRendererWidget = null; DiscordIntegrationModule.SwitchToMainMenu(); @@ -627,7 +633,7 @@ namespace Ryujinx.Ui _statusBar.Hide(); } - private void EditFooterForGameRender() + private void EditFooterForGameRenderer() { _listStatusBox.Hide(); _statusBar.Show(); @@ -635,7 +641,7 @@ namespace Ryujinx.Ui public void ToggleExtraWidgets(bool show) { - if (_glWidget != null) + if (GlRendererWidget != null) { if (show) { @@ -651,11 +657,11 @@ namespace Ryujinx.Ui } } - private static void UpdateGameMetadata(string titleId) + private void UpdateGameMetadata(string titleId) { if (_gameLoaded) { - ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => + _applicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => { DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed); double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds; @@ -665,7 +671,7 @@ namespace Ryujinx.Ui } } - public static void UpdateGraphicsConfig() + public void UpdateGraphicsConfig() { int resScale = ConfigurationState.Instance.Graphics.ResScale; float resScaleCustom = ConfigurationState.Instance.Graphics.ResScaleCustom; @@ -676,12 +682,12 @@ namespace Ryujinx.Ui Graphics.Gpu.GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache; } - public static void SaveConfig() + public void SaveConfig() { ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); } - private void End(HLE.Switch device) + private void End() { if (_ending) { @@ -690,14 +696,14 @@ namespace Ryujinx.Ui _ending = true; - if (device != null) + if (_emulationContext != null) { - UpdateGameMetadata(device.Application.TitleIdText); + UpdateGameMetadata(_emulationContext.Application.TitleIdText); - if (_glWidget != null) + if (GlRendererWidget != null) { - // We tell the widget that we are exiting - _glWidget.Exit(); + // We tell the widget that we are exiting. + GlRendererWidget.Exit(); // Wait for the other thread to dispose the HLE context before exiting. _deviceExitStatus.WaitOne(); @@ -706,64 +712,13 @@ namespace Ryujinx.Ui Dispose(); - DiscordIntegrationModule.Exit(); - - Ptc.Dispose(); - PtcProfiler.Dispose(); - - Logger.Shutdown(); - + Program.Exit(); Application.Quit(); } - private static IRenderer InitializeRenderer() - { - return new Renderer(); - } - - private static IAalOutput InitializeAudioEngine() - { - if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SoundIo) - { - if (SoundIoAudioOut.IsSupported) - { - return new SoundIoAudioOut(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, falling back to dummy audio out."); - } - } - else if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.OpenAl) - { - if (OpenALAudioOut.IsSupported) - { - return new OpenALAudioOut(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, trying to fall back to SoundIO."); - - if (SoundIoAudioOut.IsSupported) - { - Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration."); - - ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo; - SaveConfig(); - - return new SoundIoAudioOut(); - } - else - { - Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, falling back to dummy audio out."); - } - } - } - - return new DummyAudioOut(); - } - - //Events + // + // Events + // private void Application_Added(object sender, ApplicationAddedEventArgs args) { Application.Invoke(delegate @@ -797,7 +752,8 @@ namespace Ryujinx.Ui _progressBar.Value = barValue; - if (args.NumAppsLoaded == args.NumAppsFound) // Reset the vertical scrollbar to the top when titles finish loading + // Reset the vertical scrollbar to the top when titles finish loading + if (args.NumAppsLoaded == args.NumAppsFound) { _gameTableWindow.Vadjustment.Value = 0; } @@ -831,13 +787,12 @@ namespace Ryujinx.Ui { _tableStore.GetIter(out TreeIter treeIter, new TreePath(args.Path)); - string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); - - bool newToggleValue = !(bool)_tableStore.GetValue(treeIter, 0); + string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); + bool newToggleValue = !(bool)_tableStore.GetValue(treeIter, 0); _tableStore.SetValue(treeIter, 0, newToggleValue); - ApplicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => + _applicationLibrary.LoadAndSaveMetaData(titleId, appMetadata => { appMetadata.Favorite = newToggleValue; }); @@ -856,6 +811,7 @@ namespace Ryujinx.Ui private void Row_Activated(object sender, RowActivatedArgs args) { _gameTableSelection.GetSelected(out TreeIter treeIter); + string path = (string)_tableStore.GetValue(treeIter, 9); LoadApplication(path); @@ -880,81 +836,76 @@ namespace Ryujinx.Ui private void Row_Clicked(object sender, ButtonReleaseEventArgs args) { - if (args.Event.Button != 3) return; + if (args.Event.Button != 3 /* Right Click */) + { + return; + } _gameTableSelection.GetSelected(out TreeIter treeIter); - if (treeIter.UserData == IntPtr.Zero) return; + if (treeIter.UserData == IntPtr.Zero) + { + return; + } + + string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString(); + string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0]; + string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower(); BlitStruct controlData = (BlitStruct)_tableStore.GetValue(treeIter, 10); - GameTableContextMenu contextMenu = new GameTableContextMenu(_tableStore, controlData, treeIter, _virtualFileSystem); - contextMenu.ShowAll(); - contextMenu.PopupAtPointer(null); + _ = new GameTableContextMenu(this, _virtualFileSystem, titleFilePath, titleName, titleId, controlData); } private void Load_Application_File(object sender, EventArgs args) { - FileChooserDialog fileChooser = new FileChooserDialog("Choose the file to open", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept); - - fileChooser.Filter = new FileFilter(); - fileChooser.Filter.AddPattern("*.nsp" ); - fileChooser.Filter.AddPattern("*.pfs0"); - fileChooser.Filter.AddPattern("*.xci" ); - fileChooser.Filter.AddPattern("*.nca" ); - fileChooser.Filter.AddPattern("*.nro" ); - fileChooser.Filter.AddPattern("*.nso" ); - - if (fileChooser.Run() == (int)ResponseType.Accept) + using (FileChooserDialog fileChooser = new FileChooserDialog("Choose the file to open", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept)) { - LoadApplication(fileChooser.Filename); + fileChooser.Filter = new FileFilter(); + fileChooser.Filter.AddPattern("*.nsp"); + fileChooser.Filter.AddPattern("*.pfs0"); + fileChooser.Filter.AddPattern("*.xci"); + fileChooser.Filter.AddPattern("*.nca"); + fileChooser.Filter.AddPattern("*.nro"); + fileChooser.Filter.AddPattern("*.nso"); + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + LoadApplication(fileChooser.Filename); + } } - - fileChooser.Dispose(); } private void Load_Application_Folder(object sender, EventArgs args) { - FileChooserDialog fileChooser = new FileChooserDialog("Choose the folder to open", this, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept); - - if (fileChooser.Run() == (int)ResponseType.Accept) + using (FileChooserDialog fileChooser = new FileChooserDialog("Choose the folder to open", this, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept)) { - LoadApplication(fileChooser.Filename); + if (fileChooser.Run() == (int)ResponseType.Accept) + { + LoadApplication(fileChooser.Filename); + } } - - fileChooser.Dispose(); } private void Open_Ryu_Folder(object sender, EventArgs args) { - Process.Start(new ProcessStartInfo - { - FileName = AppDataManager.BaseDirPath, - UseShellExecute = true, - Verb = "open" - }); + OpenHelper.OpenFolder(AppDataManager.BaseDirPath); } private void OpenLogsFolder_Pressed(object sender, EventArgs args) { string logPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); - DirectoryInfo directory = new DirectoryInfo(logPath); - directory.Create(); + new DirectoryInfo(logPath).Create(); - Process.Start(new ProcessStartInfo - { - FileName = logPath, - UseShellExecute = true, - Verb = "open" - }); + OpenHelper.OpenFolder(logPath); } private void Exit_Pressed(object sender, EventArgs args) { if (!_gameLoaded || GtkDialog.CreateExitDialog()) { - End(_emulationContext); + End(); } } @@ -962,7 +913,7 @@ namespace Ryujinx.Ui { if (!_gameLoaded || GtkDialog.CreateExitDialog()) { - End(_emulationContext); + End(); } else { @@ -972,18 +923,12 @@ namespace Ryujinx.Ui private void StopEmulation_Pressed(object sender, EventArgs args) { - _glWidget?.Exit(); + GlRendererWidget?.Exit(); } private void Installer_File_Pressed(object o, EventArgs args) { - FileChooserDialog fileChooser = new FileChooserDialog("Choose the firmware file to open", - this, - FileChooserAction.Open, - "Cancel", - ResponseType.Cancel, - "Open", - ResponseType.Accept); + FileChooserDialog fileChooser = new FileChooserDialog("Choose the firmware file to open", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept); fileChooser.Filter = new FileFilter(); fileChooser.Filter.AddPattern("*.zip"); @@ -994,51 +939,15 @@ namespace Ryujinx.Ui private void Installer_Directory_Pressed(object o, EventArgs args) { - FileChooserDialog directoryChooser = new FileChooserDialog("Choose the firmware directory to open", - this, - FileChooserAction.SelectFolder, - "Cancel", - ResponseType.Cancel, - "Open", - ResponseType.Accept); + FileChooserDialog directoryChooser = new FileChooserDialog("Choose the firmware directory to open", this, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Open", ResponseType.Accept); HandleInstallerDialog(directoryChooser); } - private void RefreshFirmwareLabel() - { - SystemVersion currentFirmware = _contentManager.GetCurrentFirmwareVersion(); - - GLib.Idle.Add(new GLib.IdleHandler(() => - { - _firmwareVersionLabel.Text = currentFirmware != null ? currentFirmware.VersionString : "0.0.0"; - - return false; - })); - } - - private void HandleRelaunch() - { - if (_userChannelPersistence.PreviousIndex != -1 && _userChannelPersistence.ShouldRestart) - { - _userChannelPersistence.ShouldRestart = false; - - LoadApplication(_gamePath); - } - else - { - // otherwise, clear state. - _userChannelPersistence = new UserChannelPersistence(); - _gamePath = null; - } - } - private void HandleInstallerDialog(FileChooserDialog fileChooser) { if (fileChooser.Run() == (int)ResponseType.Accept) { - MessageDialog dialog = null; - try { string filename = fileChooser.Filename; @@ -1047,19 +956,11 @@ namespace Ryujinx.Ui SystemVersion firmwareVersion = _contentManager.VerifyFirmwarePackage(filename); + string dialogTitle = $"Install Firmware {firmwareVersion.VersionString}"; + if (firmwareVersion == null) { - dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, ""); - - dialog.Text = "Firmware not found."; - - dialog.SecondaryText = $"A valid system firmware was not found in {filename}."; - - Logger.Error?.Print(LogClass.Application, $"A valid system firmware was not found in {filename}."); - - dialog.Run(); - dialog.Hide(); - dialog.Dispose(); + GtkDialog.CreateErrorDialog($"A valid system firmware was not found in {filename}."); return; } @@ -1070,80 +971,49 @@ namespace Ryujinx.Ui if (currentVersion != null) { - dialogMessage += $"This will replace the current system version {currentVersion.VersionString}. "; + dialogMessage += $"\n\nThis will replace the current system version {currentVersion.VersionString}. "; } - dialogMessage += "Do you want to continue?"; - - dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, false, ""); - - dialog.Text = $"Install Firmware {firmwareVersion.VersionString}"; - dialog.SecondaryText = dialogMessage; - - int response = dialog.Run(); + dialogMessage += "\n\nDo you want to continue?"; - dialog.Dispose(); + ResponseType responseInstallDialog = (ResponseType)GtkDialog.CreateConfirmationDialog(dialogTitle, dialogMessage).Run(); - dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.None, false, ""); + MessageDialog waitingDialog = GtkDialog.CreateWaitingDialog(dialogTitle, "Installing firmware..."); - dialog.Text = $"Install Firmware {firmwareVersion.VersionString}"; - - dialog.SecondaryText = "Installing firmware..."; - - if (response == (int)ResponseType.Yes) + if (responseInstallDialog == ResponseType.Yes) { Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); Thread thread = new Thread(() => { - GLib.Idle.Add(new GLib.IdleHandler(() => + Application.Invoke(delegate { - dialog.Run(); - return false; - })); + waitingDialog.Run(); + + }); try { _contentManager.InstallFirmware(filename); - GLib.Idle.Add(new GLib.IdleHandler(() => + Application.Invoke(delegate { - dialog.Dispose(); - - dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, ""); - - dialog.Text = $"Install Firmware {firmwareVersion.VersionString}"; + waitingDialog.Dispose(); - dialog.SecondaryText = $"System version {firmwareVersion.VersionString} successfully installed."; + string message = $"System version {firmwareVersion.VersionString} successfully installed."; - Logger.Info?.Print(LogClass.Application, $"System version {firmwareVersion.VersionString} successfully installed."); - - dialog.Run(); - dialog.Dispose(); - - return false; - })); + GtkDialog.CreateInfoDialog(dialogTitle, message); + Logger.Info?.Print(LogClass.Application, message); + }); } catch (Exception ex) { - GLib.Idle.Add(new GLib.IdleHandler(() => + Application.Invoke(delegate { - dialog.Dispose(); - - dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, ""); - - dialog.Text = $"Install Firmware {firmwareVersion.VersionString} Failed."; - - dialog.SecondaryText = $"An error occured while installing system version {firmwareVersion.VersionString}." + - " Please check logs for more info."; + waitingDialog.Dispose(); - Logger.Error?.Print(LogClass.Application, ex.Message); - - dialog.Run(); - dialog.Dispose(); - - return false; - })); + GtkDialog.CreateErrorDialog(ex.Message); + }); } finally { @@ -1154,41 +1024,47 @@ namespace Ryujinx.Ui thread.Name = "GUI.FirmwareInstallerThread"; thread.Start(); } - else - { - dialog.Dispose(); - } } catch (Exception ex) { - if (dialog != null) - { - dialog.Dispose(); - } - - dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, ""); + GtkDialog.CreateErrorDialog(ex.Message); + } + } + else + { + fileChooser.Dispose(); + } + } - dialog.Text = "Parsing Firmware Failed."; + private void RefreshFirmwareLabel() + { + SystemVersion currentFirmware = _contentManager.GetCurrentFirmwareVersion(); - dialog.SecondaryText = "An error occured while parsing firmware. Please check the logs for more info."; + Application.Invoke(delegate + { + _firmwareVersionLabel.Text = currentFirmware != null ? currentFirmware.VersionString : "0.0.0"; + }); + } - Logger.Error?.Print(LogClass.Application, ex.Message); + private void HandleRelaunch() + { + if (_userChannelPersistence.PreviousIndex != -1 && _userChannelPersistence.ShouldRestart) + { + _userChannelPersistence.ShouldRestart = false; - dialog.Run(); - dialog.Dispose(); - } + LoadApplication(_currentEmulatedGamePath); } else { - fileChooser.Dispose(); + // otherwise, clear state. + _userChannelPersistence = new UserChannelPersistence(); + _currentEmulatedGamePath = null; } } private void FullScreen_Toggled(object sender, EventArgs args) { - bool fullScreenToggled = this.Window.State.HasFlag(Gdk.WindowState.Fullscreen); - - if (!fullScreenToggled) + if (!Window.State.HasFlag(Gdk.WindowState.Fullscreen)) { Fullscreen(); @@ -1211,13 +1087,15 @@ namespace Ryujinx.Ui private void Settings_Pressed(object sender, EventArgs args) { - SettingsWindow settingsWin = new SettingsWindow(_virtualFileSystem, _contentManager); - settingsWin.Show(); + new SettingsWindow(this, _virtualFileSystem, _contentManager).Show(); } private void Simulate_WakeUp_Message_Pressed(object sender, EventArgs args) { - _emulationContext.System.SimulateWakeUpMessage(); + if (_emulationContext != null) + { + _emulationContext.System.SimulateWakeUpMessage(); + } } private void Update_Pressed(object sender, EventArgs args) @@ -1230,8 +1108,7 @@ namespace Ryujinx.Ui private void About_Pressed(object sender, EventArgs args) { - AboutWindow aboutWin = new AboutWindow(); - aboutWin.Show(); + new AboutWindow().Show(); } private void Fav_Toggled(object sender, EventArgs args) @@ -1318,113 +1195,5 @@ namespace Ryujinx.Ui { UpdateGameTable(); } - - private static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b) - { - string aValue = model.GetValue(a, 5).ToString(); - string bValue = model.GetValue(b, 5).ToString(); - - if (aValue.Length > 4 && aValue.Substring(aValue.Length - 4) == "mins") - { - aValue = (float.Parse(aValue.Substring(0, aValue.Length - 5)) * 60).ToString(); - } - else if (aValue.Length > 3 && aValue.Substring(aValue.Length - 3) == "hrs") - { - aValue = (float.Parse(aValue.Substring(0, aValue.Length - 4)) * 3600).ToString(); - } - else if (aValue.Length > 4 && aValue.Substring(aValue.Length - 4) == "days") - { - aValue = (float.Parse(aValue.Substring(0, aValue.Length - 5)) * 86400).ToString(); - } - else - { - aValue = aValue.Substring(0, aValue.Length - 1); - } - - if (bValue.Length > 4 && bValue.Substring(bValue.Length - 4) == "mins") - { - bValue = (float.Parse(bValue.Substring(0, bValue.Length - 5)) * 60).ToString(); - } - else if (bValue.Length > 3 && bValue.Substring(bValue.Length - 3) == "hrs") - { - bValue = (float.Parse(bValue.Substring(0, bValue.Length - 4)) * 3600).ToString(); - } - else if (bValue.Length > 4 && bValue.Substring(bValue.Length - 4) == "days") - { - bValue = (float.Parse(bValue.Substring(0, bValue.Length - 5)) * 86400).ToString(); - } - else - { - bValue = bValue.Substring(0, bValue.Length - 1); - } - - if (float.Parse(aValue) > float.Parse(bValue)) - { - return -1; - } - else if (float.Parse(bValue) > float.Parse(aValue)) - { - return 1; - } - else - { - return 0; - } - } - - private static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b) - { - string aValue = model.GetValue(a, 6).ToString(); - string bValue = model.GetValue(b, 6).ToString(); - - if (aValue == "Never") - { - aValue = DateTime.UnixEpoch.ToString(); - } - - if (bValue == "Never") - { - bValue = DateTime.UnixEpoch.ToString(); - } - - return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue)); - } - - private static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b) - { - string aValue = model.GetValue(a, 8).ToString(); - string bValue = model.GetValue(b, 8).ToString(); - - if (aValue.Substring(aValue.Length - 2) == "GB") - { - aValue = (float.Parse(aValue[0..^2]) * 1024).ToString(); - } - else - { - aValue = aValue[0..^2]; - } - - if (bValue.Substring(bValue.Length - 2) == "GB") - { - bValue = (float.Parse(bValue[0..^2]) * 1024).ToString(); - } - else - { - bValue = bValue[0..^2]; - } - - if (float.Parse(aValue) > float.Parse(bValue)) - { - return -1; - } - else if (float.Parse(bValue) > float.Parse(aValue)) - { - return 1; - } - else - { - return 0; - } - } } -} +} \ No newline at end of file diff --git a/Ryujinx/Ui/Migration.cs b/Ryujinx/Ui/Migration.cs deleted file mode 100644 index a7a49105..00000000 --- a/Ryujinx/Ui/Migration.cs +++ /dev/null @@ -1,189 +0,0 @@ -using Gtk; -using LibHac; -using Ryujinx.Common.Configuration; -using Ryujinx.HLE.FileSystem; -using System; -using System.IO; -using System.Linq; -using System.Reflection; - -namespace Ryujinx.Ui -{ - internal class Migration - { - private VirtualFileSystem _virtualFileSystem; - - public Migration(VirtualFileSystem virtualFileSystem) - { - _virtualFileSystem = virtualFileSystem; - } - - public static bool PromptIfMigrationNeededForStartup(Window parentWindow, out bool isMigrationNeeded) - { - if (!IsMigrationNeeded()) - { - isMigrationNeeded = false; - - return true; - } - - isMigrationNeeded = true; - - int dialogResponse; - - using (MessageDialog dialog = new MessageDialog(parentWindow, DialogFlags.Modal, MessageType.Question, - ButtonsType.YesNo, "What's this?")) - { - dialog.Title = "Data Migration Needed"; - dialog.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); - dialog.Text = - "The folder structure of Ryujinx's RyuFs folder has been updated and renamed to \"Ryujinx\". " + - "Your RyuFs folder must be copied and migrated to the new \"Ryujinx\" structure. Would you like to do the migration now?\n\n" + - "Select \"Yes\" to automatically perform the migration. Your old RyuFs folder will remain as it is.\n\n" + - "Selecting \"No\" will exit Ryujinx without changing anything."; - - dialogResponse = dialog.Run(); - } - - return dialogResponse == (int)ResponseType.Yes; - } - - public static bool DoMigrationForStartup(MainWindow parentWindow, VirtualFileSystem virtualFileSystem) - { - try - { - Migration migration = new Migration(virtualFileSystem); - int saveCount = migration.Migrate(); - - using MessageDialog dialogSuccess = new MessageDialog(parentWindow, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, null) - { - Title = "Migration Success", - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"), - Text = $"Data migration was successful. {saveCount} saves were migrated.", - }; - - dialogSuccess.Run(); - - return true; - } - catch (HorizonResultException ex) - { - GtkDialog.CreateErrorDialog(ex.Message); - - return false; - } - } - - // Returns the number of saves migrated - public int Migrate() - { - // Make sure FsClient is initialized - _virtualFileSystem.Reload(); - - string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - - string oldBasePath = Path.Combine(appDataPath, "RyuFs"); - string newBasePath = Path.Combine(appDataPath, "Ryujinx"); - - string oldSaveDir = Path.Combine(oldBasePath, "nand/user/save"); - - CopyRyuFs(oldBasePath, newBasePath); - - SaveImporter importer = new SaveImporter(oldSaveDir, _virtualFileSystem.FsClient); - - return importer.Import(); - } - - private static void CopyRyuFs(string oldPath, string newPath) - { - Directory.CreateDirectory(newPath); - - CopyExcept(oldPath, newPath, "nand", "bis", "sdmc", "sdcard"); - - string oldNandPath = Path.Combine(oldPath, "nand"); - string newNandPath = Path.Combine(newPath, "bis"); - - CopyExcept(oldNandPath, newNandPath, "system", "user"); - - string oldSdPath = Path.Combine(oldPath, "sdmc"); - string newSdPath = Path.Combine(newPath, "sdcard"); - - CopyDirectory(oldSdPath, newSdPath); - - string oldSystemPath = Path.Combine(oldNandPath, "system"); - string newSystemPath = Path.Combine(newNandPath, "system"); - - CopyExcept(oldSystemPath, newSystemPath, "save"); - - string oldUserPath = Path.Combine(oldNandPath, "user"); - string newUserPath = Path.Combine(newNandPath, "user"); - - CopyExcept(oldUserPath, newUserPath, "save"); - } - - private static void CopyExcept(string srcPath, string dstPath, params string[] exclude) - { - exclude = exclude.Select(x => x.ToLowerInvariant()).ToArray(); - - DirectoryInfo srcDir = new DirectoryInfo(srcPath); - - if (!srcDir.Exists) - { - return; - } - - Directory.CreateDirectory(dstPath); - - foreach (DirectoryInfo subDir in srcDir.EnumerateDirectories()) - { - if (exclude.Contains(subDir.Name.ToLowerInvariant())) - { - continue; - } - - CopyDirectory(subDir.FullName, Path.Combine(dstPath, subDir.Name)); - } - - foreach (FileInfo file in srcDir.EnumerateFiles()) - { - file.CopyTo(Path.Combine(dstPath, file.Name)); - } - } - - private static void CopyDirectory(string srcPath, string dstPath) - { - Directory.CreateDirectory(dstPath); - - DirectoryInfo srcDir = new DirectoryInfo(srcPath); - - if (!srcDir.Exists) - { - return; - } - - Directory.CreateDirectory(dstPath); - - foreach (DirectoryInfo subDir in srcDir.EnumerateDirectories()) - { - CopyDirectory(subDir.FullName, Path.Combine(dstPath, subDir.Name)); - } - - foreach (FileInfo file in srcDir.EnumerateFiles()) - { - file.CopyTo(Path.Combine(dstPath, file.Name)); - } - } - - public static bool IsMigrationNeeded() - { - if (AppDataManager.IsCustomBasePath) return false; - - string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - - string oldBasePath = Path.Combine(appDataPath, "RyuFs"); - string newBasePath = Path.Combine(appDataPath, "Ryujinx"); - - return Directory.Exists(oldBasePath) && !Directory.Exists(newBasePath); - } - } -} diff --git a/Ryujinx/Ui/ProfileDialog.cs b/Ryujinx/Ui/ProfileDialog.cs deleted file mode 100644 index 2b26cbef..00000000 --- a/Ryujinx/Ui/ProfileDialog.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Gtk; -using System; -using System.Reflection; - -using GUI = Gtk.Builder.ObjectAttribute; - -namespace Ryujinx.Ui -{ - public class ProfileDialog : Dialog - { - public string FileName { get; private set; } - -#pragma warning disable CS0649, IDE0044 - [GUI] Entry _profileEntry; - [GUI] Label _errorMessage; -#pragma warning restore CS0649, IDE0044 - - public ProfileDialog() : this(new Builder("Ryujinx.Ui.ProfileDialog.glade")) { } - - private ProfileDialog(Builder builder) : base(builder.GetObject("_profileDialog").Handle) - { - builder.Autoconnect(this); - - Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); - } - - private void OkToggle_Activated(object sender, EventArgs args) - { - ((ToggleButton)sender).SetStateFlags(0, true); - - bool validFileName = true; - - foreach (char invalidChar in System.IO.Path.GetInvalidFileNameChars()) - { - if (_profileEntry.Text.Contains(invalidChar)) - { - validFileName = false; - } - } - - if (validFileName && !string.IsNullOrEmpty(_profileEntry.Text)) - { - FileName = $"{_profileEntry.Text}.json"; - - Respond(ResponseType.Ok); - } - else - { - _errorMessage.Text = "The file name contains invalid characters. Please try again."; - } - } - - private void CancelToggle_Activated(object sender, EventArgs args) - { - Respond(ResponseType.Cancel); - } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/ProfileDialog.glade b/Ryujinx/Ui/ProfileDialog.glade deleted file mode 100644 index adaf6608..00000000 --- a/Ryujinx/Ui/ProfileDialog.glade +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - False - Ryujinx - Profile Dialog - True - center - 400 - dialog - - - - - - False - vertical - 2 - - - False - end - - - OK - True - True - True - - - - False - True - 0 - - - - - Cancel - True - True - True - - - - False - True - 5 - 1 - - - - - False - False - 0 - - - - - True - False - vertical - - - True - False - 10 - 10 - 20 - 10 - Enter a name for the new profile: - - - True - True - 0 - - - - - True - True - 20 - 20 - 20 - - - True - True - 1 - - - - - True - False - start - 20 - 10 - 10 - 10 - - - - - - False - True - 2 - - - - - True - True - 1 - - - - - - diff --git a/Ryujinx/Ui/Resources/Controller_JoyConLeft.svg b/Ryujinx/Ui/Resources/Controller_JoyConLeft.svg new file mode 100644 index 00000000..40d06136 --- /dev/null +++ b/Ryujinx/Ui/Resources/Controller_JoyConLeft.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ryujinx/Ui/Resources/Controller_JoyConPair.svg b/Ryujinx/Ui/Resources/Controller_JoyConPair.svg new file mode 100644 index 00000000..fca94d18 --- /dev/null +++ b/Ryujinx/Ui/Resources/Controller_JoyConPair.svg @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ryujinx/Ui/Resources/Controller_JoyConRight.svg b/Ryujinx/Ui/Resources/Controller_JoyConRight.svg new file mode 100644 index 00000000..014c0ae3 --- /dev/null +++ b/Ryujinx/Ui/Resources/Controller_JoyConRight.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ryujinx/Ui/Resources/Controller_ProCon.svg b/Ryujinx/Ui/Resources/Controller_ProCon.svg new file mode 100644 index 00000000..8c2b879f --- /dev/null +++ b/Ryujinx/Ui/Resources/Controller_ProCon.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ryujinx/Ui/Resources/Icon_NCA.png b/Ryujinx/Ui/Resources/Icon_NCA.png new file mode 100644 index 00000000..6d73c8c7 Binary files /dev/null and b/Ryujinx/Ui/Resources/Icon_NCA.png differ diff --git a/Ryujinx/Ui/Resources/Icon_NRO.png b/Ryujinx/Ui/Resources/Icon_NRO.png new file mode 100644 index 00000000..bc6b65bf Binary files /dev/null and b/Ryujinx/Ui/Resources/Icon_NRO.png differ diff --git a/Ryujinx/Ui/Resources/Icon_NSO.png b/Ryujinx/Ui/Resources/Icon_NSO.png new file mode 100644 index 00000000..8782b3ea Binary files /dev/null and b/Ryujinx/Ui/Resources/Icon_NSO.png differ diff --git a/Ryujinx/Ui/Resources/Icon_NSP.png b/Ryujinx/Ui/Resources/Icon_NSP.png new file mode 100644 index 00000000..d01dc482 Binary files /dev/null and b/Ryujinx/Ui/Resources/Icon_NSP.png differ diff --git a/Ryujinx/Ui/Resources/Icon_XCI.png b/Ryujinx/Ui/Resources/Icon_XCI.png new file mode 100644 index 00000000..08f783a8 Binary files /dev/null and b/Ryujinx/Ui/Resources/Icon_XCI.png differ diff --git a/Ryujinx/Ui/Resources/Logo_Discord.png b/Ryujinx/Ui/Resources/Logo_Discord.png new file mode 100644 index 00000000..f3486b99 Binary files /dev/null and b/Ryujinx/Ui/Resources/Logo_Discord.png differ diff --git a/Ryujinx/Ui/Resources/Logo_GitHub.png b/Ryujinx/Ui/Resources/Logo_GitHub.png new file mode 100644 index 00000000..2e860709 Binary files /dev/null and b/Ryujinx/Ui/Resources/Logo_GitHub.png differ diff --git a/Ryujinx/Ui/Resources/Logo_Patreon.png b/Ryujinx/Ui/Resources/Logo_Patreon.png new file mode 100644 index 00000000..19c7ffbc Binary files /dev/null and b/Ryujinx/Ui/Resources/Logo_Patreon.png differ diff --git a/Ryujinx/Ui/Resources/Logo_Ryujinx.png b/Ryujinx/Ui/Resources/Logo_Ryujinx.png new file mode 100644 index 00000000..2fc7b017 Binary files /dev/null and b/Ryujinx/Ui/Resources/Logo_Ryujinx.png differ diff --git a/Ryujinx/Ui/Resources/Logo_Twitter.png b/Ryujinx/Ui/Resources/Logo_Twitter.png new file mode 100644 index 00000000..3d01efc0 Binary files /dev/null and b/Ryujinx/Ui/Resources/Logo_Twitter.png differ diff --git a/Ryujinx/Ui/SaveImporter.cs b/Ryujinx/Ui/SaveImporter.cs deleted file mode 100644 index 946b6cd3..00000000 --- a/Ryujinx/Ui/SaveImporter.cs +++ /dev/null @@ -1,219 +0,0 @@ -using LibHac; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Shim; -using LibHac.FsSystem; -using LibHac.Ncm; -using Ryujinx.HLE.Utilities; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; - -using ApplicationId = LibHac.Ncm.ApplicationId; - -namespace Ryujinx.Ui -{ - internal class SaveImporter - { - private FileSystemClient FsClient { get; } - private string ImportPath { get; } - - public SaveImporter(string importPath, FileSystemClient destFsClient) - { - ImportPath = importPath; - FsClient = destFsClient; - } - - // Returns the number of saves imported - public int Import() - { - return ImportSaves(FsClient, ImportPath); - } - - private static int ImportSaves(FileSystemClient fsClient, string rootSaveDir) - { - if (!Directory.Exists(rootSaveDir)) - { - return 0; - } - - SaveFinder finder = new SaveFinder(); - finder.FindSaves(rootSaveDir); - - foreach (SaveToImport save in finder.Saves) - { - Result importResult = ImportSave(fsClient, save); - - if (importResult.IsFailure()) - { - throw new HorizonResultException(importResult, $"Error importing save {save.Path}"); - } - } - - return finder.Saves.Count; - } - - private static Result ImportSave(FileSystemClient fs, SaveToImport save) - { - SaveDataAttribute key = save.Attribute; - - Result result = fs.CreateSaveData(new ApplicationId(key.ProgramId.Value), key.UserId, key.ProgramId.Value, 0, 0, 0); - if (result.IsFailure()) return result; - - bool isOldMounted = false; - bool isNewMounted = false; - - try - { - result = fs.Register("OldSave".ToU8Span(), new LocalFileSystem(save.Path)); - if (result.IsFailure()) return result; - - isOldMounted = true; - - result = fs.MountSaveData("NewSave".ToU8Span(), new ApplicationId(key.ProgramId.Value), key.UserId); - if (result.IsFailure()) return result; - - isNewMounted = true; - - result = fs.CopyDirectory("OldSave:/", "NewSave:/"); - if (result.IsFailure()) return result; - - result = fs.Commit("NewSave".ToU8Span()); - } - finally - { - if (isOldMounted) - { - fs.Unmount("OldSave".ToU8Span()); - } - - if (isNewMounted) - { - fs.Unmount("NewSave".ToU8Span()); - } - } - - return result; - } - - private class SaveFinder - { - public List Saves { get; } = new List(); - - public void FindSaves(string rootPath) - { - foreach (string subDir in Directory.EnumerateDirectories(rootPath)) - { - if (TryGetUInt64(subDir, out ulong saveDataId)) - { - SearchSaveId(subDir, saveDataId); - } - } - } - - private void SearchSaveId(string path, ulong saveDataId) - { - foreach (string subDir in Directory.EnumerateDirectories(path)) - { - if (TryGetUserId(subDir, out UserId userId)) - { - SearchUser(subDir, saveDataId, userId); - } - } - } - - private void SearchUser(string path, ulong saveDataId, UserId userId) - { - foreach (string subDir in Directory.EnumerateDirectories(path)) - { - if (TryGetUInt64(subDir, out ulong titleId) && TryGetDataPath(subDir, out string dataPath)) - { - SaveDataAttribute attribute = new SaveDataAttribute - { - Type = SaveDataType.Account, - UserId = userId, - ProgramId = new ProgramId(titleId) - }; - - SaveToImport save = new SaveToImport(dataPath, attribute); - - Saves.Add(save); - } - } - } - - private static bool TryGetDataPath(string path, out string dataPath) - { - string committedPath = Path.Combine(path, "0"); - string workingPath = Path.Combine(path, "1"); - - if (Directory.Exists(committedPath) && Directory.EnumerateFileSystemEntries(committedPath).Any()) - { - dataPath = committedPath; - return true; - } - - if (Directory.Exists(workingPath) && Directory.EnumerateFileSystemEntries(workingPath).Any()) - { - dataPath = workingPath; - return true; - } - - dataPath = default; - return false; - } - - private static bool TryGetUInt64(string path, out ulong converted) - { - string name = Path.GetFileName(path); - - if (name.Length == 16) - { - try - { - converted = Convert.ToUInt64(name, 16); - return true; - } - catch { } - } - - converted = default; - return false; - } - - private static bool TryGetUserId(string path, out UserId userId) - { - string name = Path.GetFileName(path); - - if (name.Length == 32) - { - try - { - UInt128 id = new UInt128(name); - - userId = Unsafe.As(ref id); - return true; - } - catch { } - } - - userId = default; - return false; - } - } - - private class SaveToImport - { - public string Path { get; } - public SaveDataAttribute Attribute { get; } - - public SaveToImport(string path, SaveDataAttribute attribute) - { - Path = path; - Attribute = attribute; - } - } - } -} diff --git a/Ryujinx/Ui/SettingsWindow.cs b/Ryujinx/Ui/SettingsWindow.cs deleted file mode 100644 index d17bce60..00000000 --- a/Ryujinx/Ui/SettingsWindow.cs +++ /dev/null @@ -1,577 +0,0 @@ -using Gtk; -using Ryujinx.Audio; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Configuration; -using Ryujinx.Configuration.System; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS.Services.Time.TimeZone; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Threading.Tasks; - -using GUI = Gtk.Builder.ObjectAttribute; - -namespace Ryujinx.Ui -{ - public class SettingsWindow : Window - { - private readonly VirtualFileSystem _virtualFileSystem; - private readonly ListStore _gameDirsBoxStore; - private readonly ListStore _audioBackendStore; - private readonly TimeZoneContentManager _timeZoneContentManager; - private readonly HashSet _validTzRegions; - - private long _systemTimeOffset; - -#pragma warning disable CS0649, IDE0044 - [GUI] CheckButton _errorLogToggle; - [GUI] CheckButton _warningLogToggle; - [GUI] CheckButton _infoLogToggle; - [GUI] CheckButton _stubLogToggle; - [GUI] CheckButton _debugLogToggle; - [GUI] CheckButton _fileLogToggle; - [GUI] CheckButton _guestLogToggle; - [GUI] CheckButton _fsAccessLogToggle; - [GUI] Adjustment _fsLogSpinAdjustment; - [GUI] ComboBoxText _graphicsDebugLevel; - [GUI] CheckButton _dockedModeToggle; - [GUI] CheckButton _discordToggle; - [GUI] CheckButton _checkUpdatesToggle; - [GUI] CheckButton _vSyncToggle; - [GUI] CheckButton _shaderCacheToggle; - [GUI] CheckButton _ptcToggle; - [GUI] CheckButton _fsicToggle; - [GUI] CheckButton _ignoreToggle; - [GUI] CheckButton _directKeyboardAccess; - [GUI] ComboBoxText _systemLanguageSelect; - [GUI] ComboBoxText _systemRegionSelect; - [GUI] Entry _systemTimeZoneEntry; - [GUI] EntryCompletion _systemTimeZoneCompletion; - [GUI] Box _audioBackendBox; - [GUI] ComboBox _audioBackendSelect; - [GUI] SpinButton _systemTimeYearSpin; - [GUI] SpinButton _systemTimeMonthSpin; - [GUI] SpinButton _systemTimeDaySpin; - [GUI] SpinButton _systemTimeHourSpin; - [GUI] SpinButton _systemTimeMinuteSpin; - [GUI] Adjustment _systemTimeYearSpinAdjustment; - [GUI] Adjustment _systemTimeMonthSpinAdjustment; - [GUI] Adjustment _systemTimeDaySpinAdjustment; - [GUI] Adjustment _systemTimeHourSpinAdjustment; - [GUI] Adjustment _systemTimeMinuteSpinAdjustment; - [GUI] CheckButton _custThemeToggle; - [GUI] Entry _custThemePath; - [GUI] ToggleButton _browseThemePath; - [GUI] Label _custThemePathLabel; - [GUI] TreeView _gameDirsBox; - [GUI] Entry _addGameDirBox; - [GUI] Entry _graphicsShadersDumpPath; - [GUI] ComboBoxText _anisotropy; - [GUI] ComboBoxText _aspectRatio; - [GUI] ComboBoxText _resScaleCombo; - [GUI] Entry _resScaleText; - [GUI] ToggleButton _configureController1; - [GUI] ToggleButton _configureController2; - [GUI] ToggleButton _configureController3; - [GUI] ToggleButton _configureController4; - [GUI] ToggleButton _configureController5; - [GUI] ToggleButton _configureController6; - [GUI] ToggleButton _configureController7; - [GUI] ToggleButton _configureController8; - [GUI] ToggleButton _configureControllerH; - -#pragma warning restore CS0649, IDE0044 - - public SettingsWindow(VirtualFileSystem virtualFileSystem, HLE.FileSystem.Content.ContentManager contentManager) : this(new Builder("Ryujinx.Ui.SettingsWindow.glade"), virtualFileSystem, contentManager) { } - - private SettingsWindow(Builder builder, VirtualFileSystem virtualFileSystem, HLE.FileSystem.Content.ContentManager contentManager) : base(builder.GetObject("_settingsWin").Handle) - { - builder.Autoconnect(this); - - this.Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.assets.Icon.png"); - - _virtualFileSystem = virtualFileSystem; - - _timeZoneContentManager = new TimeZoneContentManager(); - _timeZoneContentManager.InitializeInstance(virtualFileSystem, contentManager, LibHac.FsSystem.IntegrityCheckLevel.None); - - _validTzRegions = new HashSet(_timeZoneContentManager.LocationNameCache.Length, StringComparer.Ordinal); // Zone regions are identifiers. Must match exactly. - - //Bind Events - _configureController1.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player1); - _configureController2.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player2); - _configureController3.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player3); - _configureController4.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player4); - _configureController5.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player5); - _configureController6.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player6); - _configureController7.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player7); - _configureController8.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player8); - _configureControllerH.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Handheld); - _systemTimeZoneEntry.FocusOutEvent += TimeZoneEntry_FocusOut; - - _resScaleCombo.Changed += (sender, args) => _resScaleText.Visible = _resScaleCombo.ActiveId == "-1"; - - //Setup Currents - if (ConfigurationState.Instance.Logger.EnableFileLog) - { - _fileLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableError) - { - _errorLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableWarn) - { - _warningLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableInfo) - { - _infoLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableStub) - { - _stubLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableDebug) - { - _debugLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableGuest) - { - _guestLogToggle.Click(); - } - - if (ConfigurationState.Instance.Logger.EnableFsAccessLog) - { - _fsAccessLogToggle.Click(); - } - - foreach (GraphicsDebugLevel level in Enum.GetValues(typeof(GraphicsDebugLevel))) - { - _graphicsDebugLevel.Append(level.ToString(), level.ToString()); - } - - _graphicsDebugLevel.SetActiveId(ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value.ToString()); - - if (ConfigurationState.Instance.System.EnableDockedMode) - { - _dockedModeToggle.Click(); - } - - if (ConfigurationState.Instance.EnableDiscordIntegration) - { - _discordToggle.Click(); - } - - if (ConfigurationState.Instance.CheckUpdatesOnStart) - { - _checkUpdatesToggle.Click(); - } - - if (ConfigurationState.Instance.Graphics.EnableVsync) - { - _vSyncToggle.Click(); - } - - if (ConfigurationState.Instance.Graphics.EnableShaderCache) - { - _shaderCacheToggle.Click(); - } - - if (ConfigurationState.Instance.System.EnablePtc) - { - _ptcToggle.Click(); - } - - if (ConfigurationState.Instance.System.EnableFsIntegrityChecks) - { - _fsicToggle.Click(); - } - - if (ConfigurationState.Instance.System.IgnoreMissingServices) - { - _ignoreToggle.Click(); - } - - if (ConfigurationState.Instance.Hid.EnableKeyboard) - { - _directKeyboardAccess.Click(); - } - - if (ConfigurationState.Instance.Ui.EnableCustomTheme) - { - _custThemeToggle.Click(); - } - - // Custom EntryCompletion Columns. If added to glade, need to override more signals - ListStore tzList = new ListStore(typeof(string), typeof(string), typeof(string)); - _systemTimeZoneCompletion.Model = tzList; - - CellRendererText offsetCol = new CellRendererText(); - CellRendererText abbrevCol = new CellRendererText(); - - _systemTimeZoneCompletion.PackStart(offsetCol, false); - _systemTimeZoneCompletion.AddAttribute(offsetCol, "text", 0); - _systemTimeZoneCompletion.TextColumn = 1; // Regions Column - _systemTimeZoneCompletion.PackStart(abbrevCol, false); - _systemTimeZoneCompletion.AddAttribute(abbrevCol, "text", 2); - - int maxLocationLength = 0; - - foreach (var (offset, location, abbr) in _timeZoneContentManager.ParseTzOffsets()) - { - var hours = Math.DivRem(offset, 3600, out int seconds); - var minutes = Math.Abs(seconds) / 60; - - var abbr2 = (abbr.StartsWith('+') || abbr.StartsWith('-')) ? string.Empty : abbr; - - tzList.AppendValues($"UTC{hours:+0#;-0#;+00}:{minutes:D2} ", location, abbr2); - _validTzRegions.Add(location); - - maxLocationLength = Math.Max(maxLocationLength, location.Length); - } - - _systemTimeZoneEntry.WidthChars = Math.Max(20, maxLocationLength + 1); // Ensure minimum Entry width - _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(); - - _systemTimeZoneCompletion.MatchFunc = TimeZoneMatchFunc; - - _systemLanguageSelect.SetActiveId(ConfigurationState.Instance.System.Language.Value.ToString()); - _systemRegionSelect.SetActiveId(ConfigurationState.Instance.System.Region.Value.ToString()); - _resScaleCombo.SetActiveId(ConfigurationState.Instance.Graphics.ResScale.Value.ToString()); - _anisotropy.SetActiveId(ConfigurationState.Instance.Graphics.MaxAnisotropy.Value.ToString()); - _aspectRatio.SetActiveId(((int)ConfigurationState.Instance.Graphics.AspectRatio.Value).ToString()); - - _custThemePath.Buffer.Text = ConfigurationState.Instance.Ui.CustomThemePath; - _resScaleText.Buffer.Text = ConfigurationState.Instance.Graphics.ResScaleCustom.Value.ToString(); - _resScaleText.Visible = _resScaleCombo.ActiveId == "-1"; - _graphicsShadersDumpPath.Buffer.Text = ConfigurationState.Instance.Graphics.ShadersDumpPath; - _fsLogSpinAdjustment.Value = ConfigurationState.Instance.System.FsGlobalAccessLogMode; - _systemTimeOffset = ConfigurationState.Instance.System.SystemTimeOffset; - - _gameDirsBox.AppendColumn("", new CellRendererText(), "text", 0); - _gameDirsBoxStore = new ListStore(typeof(string)); - _gameDirsBox.Model = _gameDirsBoxStore; - - foreach (string gameDir in ConfigurationState.Instance.Ui.GameDirs.Value) - { - _gameDirsBoxStore.AppendValues(gameDir); - } - - if (_custThemeToggle.Active == false) - { - _custThemePath.Sensitive = false; - _custThemePathLabel.Sensitive = false; - _browseThemePath.Sensitive = false; - } - - //Setup system time spinners - UpdateSystemTimeSpinners(); - - _audioBackendStore = new ListStore(typeof(string), typeof(AudioBackend)); - - TreeIter openAlIter = _audioBackendStore.AppendValues("OpenAL", AudioBackend.OpenAl); - TreeIter soundIoIter = _audioBackendStore.AppendValues("SoundIO", AudioBackend.SoundIo); - TreeIter dummyIter = _audioBackendStore.AppendValues("Dummy", AudioBackend.Dummy); - - _audioBackendSelect = ComboBox.NewWithModelAndEntry(_audioBackendStore); - _audioBackendSelect.EntryTextColumn = 0; - _audioBackendSelect.Entry.IsEditable = false; - - switch (ConfigurationState.Instance.System.AudioBackend.Value) - { - case AudioBackend.OpenAl: - _audioBackendSelect.SetActiveIter(openAlIter); - break; - case AudioBackend.SoundIo: - _audioBackendSelect.SetActiveIter(soundIoIter); - break; - case AudioBackend.Dummy: - _audioBackendSelect.SetActiveIter(dummyIter); - break; - default: - throw new ArgumentOutOfRangeException(); - } - - _audioBackendBox.Add(_audioBackendSelect); - _audioBackendSelect.Show(); - - bool openAlIsSupported = false; - bool soundIoIsSupported = false; - - Task.Run(() => - { - openAlIsSupported = OpenALAudioOut.IsSupported; - soundIoIsSupported = SoundIoAudioOut.IsSupported; - }); - - // This function runs whenever the dropdown is opened - _audioBackendSelect.SetCellDataFunc(_audioBackendSelect.Cells[0], (layout, cell, model, iter) => - { - cell.Sensitive = ((AudioBackend)_audioBackendStore.GetValue(iter, 1)) switch - { - AudioBackend.OpenAl => openAlIsSupported, - AudioBackend.SoundIo => soundIoIsSupported, - AudioBackend.Dummy => true, - _ => throw new ArgumentOutOfRangeException() - }; - }); - } - - private void UpdateSystemTimeSpinners() - { - //Bind system time events - _systemTimeYearSpin.ValueChanged -= SystemTimeSpin_ValueChanged; - _systemTimeMonthSpin.ValueChanged -= SystemTimeSpin_ValueChanged; - _systemTimeDaySpin.ValueChanged -= SystemTimeSpin_ValueChanged; - _systemTimeHourSpin.ValueChanged -= SystemTimeSpin_ValueChanged; - _systemTimeMinuteSpin.ValueChanged -= SystemTimeSpin_ValueChanged; - - //Apply actual system time + SystemTimeOffset to system time spin buttons - DateTime systemTime = DateTime.Now.AddSeconds(_systemTimeOffset); - - _systemTimeYearSpinAdjustment.Value = systemTime.Year; - _systemTimeMonthSpinAdjustment.Value = systemTime.Month; - _systemTimeDaySpinAdjustment.Value = systemTime.Day; - _systemTimeHourSpinAdjustment.Value = systemTime.Hour; - _systemTimeMinuteSpinAdjustment.Value = systemTime.Minute; - - //Format spin buttons text to include leading zeros - _systemTimeYearSpin.Text = systemTime.Year.ToString("0000"); - _systemTimeMonthSpin.Text = systemTime.Month.ToString("00"); - _systemTimeDaySpin.Text = systemTime.Day.ToString("00"); - _systemTimeHourSpin.Text = systemTime.Hour.ToString("00"); - _systemTimeMinuteSpin.Text = systemTime.Minute.ToString("00"); - - //Bind system time events - _systemTimeYearSpin.ValueChanged += SystemTimeSpin_ValueChanged; - _systemTimeMonthSpin.ValueChanged += SystemTimeSpin_ValueChanged; - _systemTimeDaySpin.ValueChanged += SystemTimeSpin_ValueChanged; - _systemTimeHourSpin.ValueChanged += SystemTimeSpin_ValueChanged; - _systemTimeMinuteSpin.ValueChanged += SystemTimeSpin_ValueChanged; - } - - private void SaveSettings() - { - List gameDirs = new List(); - - _gameDirsBoxStore.GetIterFirst(out TreeIter treeIter); - for (int i = 0; i < _gameDirsBoxStore.IterNChildren(); i++) - { - gameDirs.Add((string)_gameDirsBoxStore.GetValue(treeIter, 0)); - - _gameDirsBoxStore.IterNext(ref treeIter); - } - - if (!float.TryParse(_resScaleText.Buffer.Text, out float resScaleCustom) || resScaleCustom <= 0.0f) - { - resScaleCustom = 1.0f; - } - - if (_validTzRegions.Contains(_systemTimeZoneEntry.Text)) - { - ConfigurationState.Instance.System.TimeZone.Value = _systemTimeZoneEntry.Text; - } - - ConfigurationState.Instance.Logger.EnableError.Value = _errorLogToggle.Active; - ConfigurationState.Instance.Logger.EnableWarn.Value = _warningLogToggle.Active; - ConfigurationState.Instance.Logger.EnableInfo.Value = _infoLogToggle.Active; - ConfigurationState.Instance.Logger.EnableStub.Value = _stubLogToggle.Active; - ConfigurationState.Instance.Logger.EnableDebug.Value = _debugLogToggle.Active; - ConfigurationState.Instance.Logger.EnableGuest.Value = _guestLogToggle.Active; - ConfigurationState.Instance.Logger.EnableFsAccessLog.Value = _fsAccessLogToggle.Active; - ConfigurationState.Instance.Logger.EnableFileLog.Value = _fileLogToggle.Active; - ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value = Enum.Parse(_graphicsDebugLevel.ActiveId); - ConfigurationState.Instance.System.EnableDockedMode.Value = _dockedModeToggle.Active; - ConfigurationState.Instance.EnableDiscordIntegration.Value = _discordToggle.Active; - ConfigurationState.Instance.CheckUpdatesOnStart.Value = _checkUpdatesToggle.Active; - ConfigurationState.Instance.Graphics.EnableVsync.Value = _vSyncToggle.Active; - ConfigurationState.Instance.Graphics.EnableShaderCache.Value = _shaderCacheToggle.Active; - ConfigurationState.Instance.System.EnablePtc.Value = _ptcToggle.Active; - ConfigurationState.Instance.System.EnableFsIntegrityChecks.Value = _fsicToggle.Active; - ConfigurationState.Instance.System.IgnoreMissingServices.Value = _ignoreToggle.Active; - ConfigurationState.Instance.Hid.EnableKeyboard.Value = _directKeyboardAccess.Active; - ConfigurationState.Instance.Ui.EnableCustomTheme.Value = _custThemeToggle.Active; - ConfigurationState.Instance.System.Language.Value = Enum.Parse(_systemLanguageSelect.ActiveId); - ConfigurationState.Instance.System.Region.Value = Enum.Parse(_systemRegionSelect.ActiveId); - ConfigurationState.Instance.System.SystemTimeOffset.Value = _systemTimeOffset; - ConfigurationState.Instance.Ui.CustomThemePath.Value = _custThemePath.Buffer.Text; - ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = _graphicsShadersDumpPath.Buffer.Text; - ConfigurationState.Instance.Ui.GameDirs.Value = gameDirs; - ConfigurationState.Instance.System.FsGlobalAccessLogMode.Value = (int)_fsLogSpinAdjustment.Value; - ConfigurationState.Instance.Graphics.MaxAnisotropy.Value = float.Parse(_anisotropy.ActiveId, CultureInfo.InvariantCulture); - ConfigurationState.Instance.Graphics.AspectRatio.Value = Enum.Parse(_aspectRatio.ActiveId); - ConfigurationState.Instance.Graphics.ResScale.Value = int.Parse(_resScaleCombo.ActiveId); - ConfigurationState.Instance.Graphics.ResScaleCustom.Value = resScaleCustom; - - if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter)) - { - ConfigurationState.Instance.System.AudioBackend.Value = (AudioBackend)_audioBackendStore.GetValue(activeIter, 1); - } - - MainWindow.SaveConfig(); - MainWindow.UpdateGraphicsConfig(); - MainWindow.ApplyTheme(); - } - - //Events - private void TimeZoneEntry_FocusOut(object sender, FocusOutEventArgs e) - { - if (!_validTzRegions.Contains(_systemTimeZoneEntry.Text)) - { - _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(); - } - } - - private bool TimeZoneMatchFunc(EntryCompletion compl, string key, TreeIter iter) - { - key = key.Trim().Replace(' ', '_'); - - return ((string)compl.Model.GetValue(iter, 1)).Contains(key, StringComparison.OrdinalIgnoreCase) || // region - ((string)compl.Model.GetValue(iter, 2)).StartsWith(key, StringComparison.OrdinalIgnoreCase) || // abbr - ((string)compl.Model.GetValue(iter, 0)).Substring(3).StartsWith(key); // offset - } - - private void SystemTimeSpin_ValueChanged(object sender, EventArgs e) - { - int year = _systemTimeYearSpin.ValueAsInt; - int month = _systemTimeMonthSpin.ValueAsInt; - int day = _systemTimeDaySpin.ValueAsInt; - int hour = _systemTimeHourSpin.ValueAsInt; - int minute = _systemTimeMinuteSpin.ValueAsInt; - - if (!DateTime.TryParse(year + "-" + month + "-" + day + " " + hour + ":" + minute, out DateTime newTime)) - { - UpdateSystemTimeSpinners(); - - return; - } - - newTime = newTime.AddSeconds(DateTime.Now.Second).AddMilliseconds(DateTime.Now.Millisecond); - - long systemTimeOffset = (long)Math.Ceiling((newTime - DateTime.Now).TotalMinutes) * 60L; - - if (_systemTimeOffset != systemTimeOffset) - { - _systemTimeOffset = systemTimeOffset; - UpdateSystemTimeSpinners(); - } - } - - private void AddDir_Pressed(object sender, EventArgs args) - { - if (Directory.Exists(_addGameDirBox.Buffer.Text)) - { - _gameDirsBoxStore.AppendValues(_addGameDirBox.Buffer.Text); - } - else - { - FileChooserDialog fileChooser = new FileChooserDialog("Choose the game directory to add to the list", this, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept) - { - SelectMultiple = true - }; - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - foreach (string directory in fileChooser.Filenames) - { - bool directoryAdded = false; - - if (_gameDirsBoxStore.GetIterFirst(out TreeIter treeIter)) - { - do - { - if (directory.Equals((string)_gameDirsBoxStore.GetValue(treeIter, 0))) - { - directoryAdded = true; - break; - } - } while(_gameDirsBoxStore.IterNext(ref treeIter)); - } - - if (!directoryAdded) - { - _gameDirsBoxStore.AppendValues(directory); - } - } - } - - fileChooser.Dispose(); - } - - _addGameDirBox.Buffer.Text = ""; - - ((ToggleButton)sender).SetStateFlags(0, true); - } - - private void RemoveDir_Pressed(object sender, EventArgs args) - { - TreeSelection selection = _gameDirsBox.Selection; - - if (selection.GetSelected(out TreeIter treeIter)) - { - _gameDirsBoxStore.Remove(ref treeIter); - } - - ((ToggleButton)sender).SetStateFlags(0, true); - } - - private void CustThemeToggle_Activated(object sender, EventArgs args) - { - _custThemePath.Sensitive = _custThemeToggle.Active; - _custThemePathLabel.Sensitive = _custThemeToggle.Active; - _browseThemePath.Sensitive = _custThemeToggle.Active; - } - - private void BrowseThemeDir_Pressed(object sender, EventArgs args) - { - FileChooserDialog fileChooser = new FileChooserDialog("Choose the theme to load", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Select", ResponseType.Accept); - - fileChooser.Filter = new FileFilter(); - fileChooser.Filter.AddPattern("*.css"); - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - _custThemePath.Buffer.Text = fileChooser.Filename; - } - - fileChooser.Dispose(); - - _browseThemePath.SetStateFlags(0, true); - } - - private void ConfigureController_Pressed(object sender, EventArgs args, PlayerIndex playerIndex) - { - ((ToggleButton)sender).SetStateFlags(0, true); - - ControllerWindow controllerWin = new ControllerWindow(playerIndex, _virtualFileSystem); - controllerWin.Show(); - } - - private void SaveToggle_Activated(object sender, EventArgs args) - { - SaveSettings(); - Dispose(); - } - - private void ApplyToggle_Activated(object sender, EventArgs args) - { - SaveSettings(); - } - - private void CloseToggle_Activated(object sender, EventArgs args) - { - Dispose(); - } - } -} diff --git a/Ryujinx/Ui/SettingsWindow.glade b/Ryujinx/Ui/SettingsWindow.glade deleted file mode 100644 index 936aa520..00000000 --- a/Ryujinx/Ui/SettingsWindow.glade +++ /dev/null @@ -1,2411 +0,0 @@ - - - - - - 3 - 1 - 10 - - - 1 - 31 - 1 - 5 - - - 23 - 1 - 5 - - - 59 - 1 - 5 - - - 1 - 12 - 1 - 5 - - - 2000 - 2060 - 1 - 10 - - - 0 - True - True - - - False - Ryujinx - Settings - True - center - 650 - 550 - - - True - False - vertical - - - True - True - in - - - True - False - - - True - True - - - True - False - 5 - 10 - 5 - vertical - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - General - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - Enable Discord Rich Presence - True - True - False - Enables or disables Discord Rich Presence - start - True - - - False - True - 5 - 0 - - - - - Check for Updates on Launch - True - True - False - start - True - - - False - True - 5 - 1 - - - - - True - True - 1 - - - - - False - True - 5 - 1 - - - - - True - False - 5 - 5 - - - False - True - 5 - 2 - - - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - Game Directories - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - True - True - 10 - in - - - True - True - False - False - - - - - - - - - True - True - 0 - - - - - True - False - - - True - True - Enter a game directroy to add to the list - - - True - True - 0 - - - - - Add - 80 - True - True - True - Add a game directory to the list - 5 - - - - False - True - 1 - - - - - Remove - 80 - True - True - True - Remove selected game directory - 5 - - - - False - True - 3 - - - - - False - True - 1 - - - - - True - True - 1 - - - - - True - True - 5 - 4 - - - - - True - False - 5 - 5 - - - False - True - 5 - 5 - - - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - Themes - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - Use Custom Theme - True - True - False - Enable or disable custom themes in the GUI - start - True - - - - False - True - 5 - 1 - - - - - True - False - - - True - False - Path to custom GUI theme - Custom Theme Path: - - - False - True - 5 - 0 - - - - - True - True - Path to custom GUI theme - center - - - True - True - 1 - - - - - Browse... - 80 - True - True - True - Browse for a custom GUI theme - 5 - - - - False - True - 2 - - - - - False - True - 10 - 2 - - - - - False - True - 1 - - - - - False - True - 5 - 6 - - - - - - - True - False - General - - - False - - - - - True - False - 5 - 10 - 5 - vertical - - - True - False - 5 - 5 - - - Enable Docked Mode - True - True - False - Enable or disable Docked Mode - True - - - False - True - 10 - 0 - - - - - Direct Keyboard Access - True - True - False - Enable or disable "direct keyboard access (HID) support" (Provides games access to your keyboard as a text entry device) - True - - - False - False - 10 - 1 - - - - - False - True - 5 - 0 - - - - - True - False - - - False - True - 1 - - - - - - True - False - center - center - 20 - - - True - False - vertical - - - True - False - 20 - 20 - Player 1 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 0 - 0 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 3 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 4 - 0 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 2 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 2 - 0 - - - - - True - False - vertical - - - True - False - 20 - 20 - Handheld - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 4 - 4 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 6 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 4 - 2 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 5 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 2 - 2 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 7 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 0 - 4 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 4 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 0 - 2 - - - - - True - False - vertical - - - True - False - 20 - 20 - Player 8 - - - False - True - 0 - - - - - Configure - True - True - True - 20 - 20 - 20 - 20 - - - False - True - 1 - - - - - 2 - 4 - - - - - True - False - - - 1 - 0 - - - - - True - False - - - 3 - 0 - - - - - True - False - - - 3 - 2 - - - - - True - False - - - 3 - 4 - - - - - True - False - - - 1 - 2 - - - - - True - False - - - 1 - 4 - - - - - True - False - - - 1 - 1 - - - - - True - False - - - 1 - 3 - - - - - True - False - - - 3 - 1 - - - - - True - False - - - 3 - 3 - - - - - True - False - - - 0 - 1 - - - - - True - False - - - 2 - 1 - - - - - True - False - - - 4 - 1 - - - - - True - False - - - 0 - 3 - - - - - True - False - - - 2 - 3 - - - - - True - False - - - 4 - 3 - - - - - True - True - 2 - - - - - True - False - - - False - True - 3 - - - - - 1 - - - - - True - False - Input - - - 1 - False - - - - - True - False - 5 - 10 - 5 - vertical - - - True - False - start - 5 - 5 - vertical - - - True - False - start - 5 - Core - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - True - False - - - True - False - Change System Region - end - System Region: - - - False - True - 5 - 2 - - - - - True - False - Change System Region - 5 - - Japan - USA - Europe - Australia - China - Korea - Taiwan - - - - False - True - 3 - - - - - False - True - 5 - 0 - - - - - True - False - - - True - False - Change System Language - end - System Language: - - - False - True - 5 - 0 - - - - - True - False - Change System Language - - American English - British English - Canadian French - Chinese - Dutch - French - German - Italian - Japanese - Korean - Latin American Spanish - Portuguese - Russian - Simplified Chinese - Spanish - Taiwanese - Traditional Chinese - - - - False - True - 1 - - - - - False - True - 5 - 1 - - - - - True - False - - - True - False - Change System TimeZone - end - System TimeZone: - - - False - True - 5 - 1 - - - - - True - True - Change System TimeZone - 5 - _systemTimeZoneCompletion - - - False - True - 2 - - - - - False - True - 5 - 2 - - - - - True - False - - - True - False - end - System Time: - - - False - True - 5 - 0 - - - - - True - True - 2000 - vertical - _systemTimeYearSpinAdjustment - True - 2000 - - - False - True - 1 - - - - - True - False - end - - - - - False - True - 5 - 2 - - - - - True - True - 1 - vertical - _systemTimeMonthSpinAdjustment - True - 1 - - - False - True - 3 - - - - - True - False - end - - - - - False - True - 5 - 4 - - - - - True - True - 1 - vertical - _systemTimeDaySpinAdjustment - True - 1 - - - False - True - 5 - - - - - True - True - 0 - vertical - _systemTimeHourSpinAdjustment - True - - - False - True - 6 - - - - - True - False - end - : - - - False - True - 5 - 7 - - - - - True - True - 0 - vertical - _systemTimeMinuteSpinAdjustment - True - - - False - True - 8 - - - - - False - True - 5 - 3 - - - - - Enable VSync - True - True - False - Enables or disables Vertical Sync - start - 5 - 5 - True - - - False - True - 4 - - - - - Enable Profiled Persistent Translation Cache - True - True - False - Enables or disables profiled translation cache persistency - start - 5 - 5 - True - - - False - True - 6 - - - - - Enable FS Integrity Checks - True - True - False - Enables integrity checks on Game content files - start - 5 - 5 - True - - - False - True - 7 - - - - - True - True - 1 - - - - - True - False - - - - - - True - False - Change System Region - end - 5 - Audio Backend: - - - False - True - 5 - 2 - - - - - False - True - 5 - 2 - - - - - False - True - 5 - 0 - - - - - True - False - 5 - 5 - - - False - True - 5 - 1 - - - - - True - False - start - 5 - 5 - vertical - - - True - False - - - True - False - start - 5 - Hacks - - - - - - False - True - 0 - - - - - True - False - start - 5 - - These may cause instability - - - False - True - 1 - - - - - False - True - 1 - - - - - True - False - 10 - 10 - vertical - - - Ignore Missing Services - True - True - False - Enable or disable ignoring missing services - start - 5 - 5 - True - - - False - True - 0 - - - - - True - True - 2 - - - - - False - True - 5 - 4 - - - - - 2 - - - - - True - False - end - System - - - 2 - False - - - - - True - False - 5 - vertical - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - 5 - 5 - Enhancements - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - Enable Shader Cache - True - True - False - Enables or disables Shader Cache - start - 5 - 5 - True - - - False - True - 0 - - - - - True - False - 5 - 5 - - - True - False - Resolution Scale applied to applicable render targets. - Resolution Scale: - - - False - True - 5 - 0 - - - - - True - False - Resolution Scale applied to applicable render targets. - 1 - - Native (720p/1080p) - 2x (1440p/2160p) - 3x (2160p/3240p) - 4x (2880p/4320p) - Custom (not recommended) - - - - False - True - 1 - - - - - True - True - Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash. - center - False - 1.0 - number - - - True - True - 2 - - - - - False - True - 5 - 1 - - - - - True - False - 5 - 5 - - - True - False - Level of Anisotropic Filtering (set to Auto to use the value requested by the game) - Anisotropic Filtering: - - - False - True - 5 - 0 - - - - - True - False - Level of Anisotropic Filtering (set to Auto to use the value requested by the game) - -1 - - Auto - 2x - 4x - 8x - 16x - - - - False - True - 1 - - - - - False - True - 5 - 1 - - - - - True - False - 5 - 5 - - - True - False - Aspect Ratio applied to the renderer window. - Aspect Ratio: - - - False - True - 5 - 0 - - - - - True - False - Aspect Ratio applied to the renderer window. - 1 - - 4:3 - 16:9 - 16:10 - 21:9 - 32:9 - Stretch to Fit Window - - - - False - True - 1 - - - - - False - True - 5 - 3 - - - - - False - True - 2 - - - - - False - True - 5 - 0 - - - - - True - False - - - False - True - 5 - 1 - - - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - 5 - 5 - Developer Options - - - - - - False - True - 0 - - - - - True - False - 10 - 10 - vertical - - - True - False - 5 - 5 - - - True - False - Graphics Shaders Dump Path - Graphics Shaders Dump Path: - - - False - True - 5 - 0 - - - - - True - True - Graphics Shaders Dump Path - center - False - - - True - True - 1 - - - - - False - True - 5 - 0 - - - - - False - True - 1 - - - - - False - True - 5 - 4 - - - - - 3 - - - - - True - False - Graphics - - - 3 - False - - - - - True - False - 5 - 10 - 5 - vertical - - - True - False - 5 - 5 - vertical - - - True - False - start - 5 - Logging - - - - - - False - True - 0 - - - - - True - False - start - 10 - 10 - vertical - - - Enable Logging to File - True - True - False - Enables or disables logging to a file on disk - start - 5 - 5 - True - - - False - True - 0 - - - - - Enable Stub Logs - True - True - False - Enables printing stub log messages - start - 5 - 5 - True - - - False - True - 3 - - - - - Enable Info Logs - True - True - False - Enables printing info log messages - start - 5 - 5 - True - - - False - True - 4 - - - - - Enable Warning Logs - True - True - False - Enables printing warning log messages - start - 5 - 5 - True - - - False - True - 5 - - - - - Enable Error Logs - True - True - False - Enables printing error log messages - start - 5 - 5 - True - - - False - True - 6 - - - - - Enable Guest Logs - True - True - False - Enables printing guest log messages - start - 5 - 5 - True - - - False - True - 7 - - - - - Enable Fs Access Logs - True - True - False - Enables printing fs access log messages - start - 5 - 5 - True - - - False - True - 8 - - - - - True - False - - - True - False - Enables FS access log output to the console. Possible modes are 0-3 - Fs Global Access Log Mode: - - - False - True - 5 - 0 - - - - - True - True - Enables FS access log output to the console. Possible modes are 0-3 - 0 - _fsLogSpinAdjustment - - - True - True - 1 - - - - - False - True - 5 - 9 - - - - - True - True - 1 - - - - - False - True - 5 - 0 - - - - - True - False - 5 - 5 - 10 - vertical - - - True - False - Use with care - start - 5 - Developer Options (WARNING: Will reduce performance) - - - - - - False - True - 0 - - - - - True - False - start - 10 - 10 - vertical - - - True - False - 5 - - - True - False - Requires appropriate log levels enabled. - OpenGL Log Level - - - False - True - 5 - 22 - - - - - True - False - Requires appropriate log levels enabled. - 5 - - - False - True - 22 - - - - - False - True - 1 - - - - - Enable Debug Logs - True - True - False - Enables printing debug log messages - start - 5 - 5 - True - - - False - True - 21 - - - - - False - True - 1 - - - - - False - True - 5 - 22 - - - - - 4 - - - - - True - False - Logging - - - 4 - False - - - - - - - - - True - True - 0 - - - - - True - False - 5 - 3 - 3 - 5 - end - - - Save - True - True - True - - - - False - False - 0 - - - - - Close - True - True - True - - - - False - False - 1 - - - - - Apply - True - True - True - - - - True - True - 2 - - - - - False - False - 1 - - - - - - diff --git a/Ryujinx/Ui/StatusUpdatedEventArgs.cs b/Ryujinx/Ui/StatusUpdatedEventArgs.cs index a028c8f8..c9fba77b 100644 --- a/Ryujinx/Ui/StatusUpdatedEventArgs.cs +++ b/Ryujinx/Ui/StatusUpdatedEventArgs.cs @@ -21,4 +21,4 @@ namespace Ryujinx.Ui GpuName = gpuName; } } -} +} \ No newline at end of file diff --git a/Ryujinx/Ui/TitleUpdateWindow.cs b/Ryujinx/Ui/TitleUpdateWindow.cs deleted file mode 100644 index 54b5b262..00000000 --- a/Ryujinx/Ui/TitleUpdateWindow.cs +++ /dev/null @@ -1,208 +0,0 @@ -using Gtk; -using LibHac; -using LibHac.Common; -using LibHac.Fs; -using LibHac.Fs.Fsa; -using LibHac.FsSystem; -using LibHac.FsSystem.NcaUtils; -using LibHac.Ns; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Logging; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -using GUI = Gtk.Builder.ObjectAttribute; -using JsonHelper = Ryujinx.Common.Utilities.JsonHelper; - -namespace Ryujinx.Ui -{ - public class TitleUpdateWindow : Window - { - private readonly VirtualFileSystem _virtualFileSystem; - private readonly string _titleId; - private readonly string _updateJsonPath; - - private TitleUpdateMetadata _titleUpdateWindowData; - private Dictionary _radioButtonToPathDictionary; - -#pragma warning disable CS0649, IDE0044 - [GUI] Label _baseTitleInfoLabel; - [GUI] Box _availableUpdatesBox; - [GUI] RadioButton _noUpdateRadioButton; -#pragma warning restore CS0649, IDE0044 - - public TitleUpdateWindow(string titleId, string titleName, VirtualFileSystem virtualFileSystem) : this(new Builder("Ryujinx.Ui.TitleUpdateWindow.glade"), titleId, titleName, virtualFileSystem) { } - - private TitleUpdateWindow(Builder builder, string titleId, string titleName, VirtualFileSystem virtualFileSystem) : base(builder.GetObject("_titleUpdateWindow").Handle) - { - builder.Autoconnect(this); - - _titleId = titleId; - _virtualFileSystem = virtualFileSystem; - _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json"); - _radioButtonToPathDictionary = new Dictionary(); - - try - { - _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath); - } - catch - { - _titleUpdateWindowData = new TitleUpdateMetadata - { - Selected = "", - Paths = new List() - }; - } - - _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]"; - _noUpdateRadioButton.Active = true; - - foreach (string path in _titleUpdateWindowData.Paths) - { - AddUpdate(path, false); - } - - foreach ((RadioButton update, var _) in _radioButtonToPathDictionary.Where(keyValuePair => keyValuePair.Value == _titleUpdateWindowData.Selected)) - { - update.Active = true; - } - } - - private void AddUpdate(string path, bool showErrorDialog = true) - { - if (File.Exists(path)) - { - using (FileStream file = new FileStream(path, FileMode.Open, FileAccess.Read)) - { - PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); - - try - { - (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); - - if (controlNca != null && patchNca != null) - { - ApplicationControlProperty controlData = new ApplicationControlProperty(); - - controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); - nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); - - RadioButton radioButton = new RadioButton($"Version {controlData.DisplayVersion.ToString()} - {path}"); - radioButton.JoinGroup(_noUpdateRadioButton); - - _availableUpdatesBox.Add(radioButton); - _radioButtonToPathDictionary.Add(radioButton, path); - - radioButton.Show(); - radioButton.Active = true; - } - else - { - GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); - } - } - catch (InvalidDataException exception) - { - Logger.Error?.Print(LogClass.Application, $"{exception.Message}. Errored File: {path}"); - - if (showErrorDialog) - { - GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add Update Failed!", "The NCA header content type check has failed. This is usually because the header key is incorrect or missing."); - } - } - catch (MissingKeyException exception) - { - Logger.Error?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}. Errored File: {path}"); - - if (showErrorDialog) - { - GtkDialog.CreateInfoDialog("Ryujinx - Error", "Add Update Failed!", $"Your key set is missing a key with the name: {exception.Name}"); - } - } - } - } - } - - private void RemoveUpdates(bool removeSelectedOnly = false) - { - foreach (RadioButton radioButton in _noUpdateRadioButton.Group) - { - if (radioButton.Label != "No Update" && (!removeSelectedOnly || radioButton.Active)) - { - _availableUpdatesBox.Remove(radioButton); - _radioButtonToPathDictionary.Remove(radioButton); - radioButton.Dispose(); - } - } - } - - private void AddButton_Clicked(object sender, EventArgs args) - { - FileChooserDialog fileChooser = new FileChooserDialog("Select update files", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept) - { - SelectMultiple = true, - Filter = new FileFilter() - }; - fileChooser.SetPosition(WindowPosition.Center); - fileChooser.Filter.AddPattern("*.nsp"); - - if (fileChooser.Run() == (int)ResponseType.Accept) - { - foreach (string path in fileChooser.Filenames) - { - AddUpdate(path); - } - } - - fileChooser.Dispose(); - } - - private void RemoveButton_Clicked(object sender, EventArgs args) - { - RemoveUpdates(true); - } - - private void RemoveAllButton_Clicked(object sender, EventArgs args) - { - RemoveUpdates(); - } - - private void SaveButton_Clicked(object sender, EventArgs args) - { - _titleUpdateWindowData.Paths.Clear(); - _titleUpdateWindowData.Selected = ""; - - foreach (string paths in _radioButtonToPathDictionary.Values) - { - _titleUpdateWindowData.Paths.Add(paths); - } - - foreach (RadioButton radioButton in _noUpdateRadioButton.Group) - { - if (radioButton.Active) - { - _titleUpdateWindowData.Selected = _radioButtonToPathDictionary.TryGetValue(radioButton, out string updatePath) ? updatePath : ""; - } - } - - using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough)) - { - dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true))); - } - - MainWindow.UpdateGameTable(); - Dispose(); - } - - private void CancelButton_Clicked(object sender, EventArgs args) - { - Dispose(); - } - } -} \ No newline at end of file diff --git a/Ryujinx/Ui/TitleUpdateWindow.glade b/Ryujinx/Ui/TitleUpdateWindow.glade deleted file mode 100644 index de557471..00000000 --- a/Ryujinx/Ui/TitleUpdateWindow.glade +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - False - Ryujinx - Title Update Manager - True - center - 550 - 250 - - - True - False - vertical - - - True - False - vertical - - - True - False - 10 - 10 - 10 - 10 - Available Updates - - - False - True - 0 - - - - - True - True - 10 - 10 - in - - - True - False - - - True - False - vertical - - - No Update - True - True - False - True - True - - - False - True - 0 - - - - - - - - - True - True - 1 - - - - - True - True - 0 - - - - - True - False - - - True - False - 10 - 10 - start - - - Add - True - True - True - Adds an update to this list - 10 - - - - True - True - 0 - - - - - Remove - True - True - True - Removes the selected update - 10 - - - - True - True - 1 - - - - - Remove All - True - True - True - Removes the selected update - 10 - - - - True - True - 2 - - - - - True - True - 0 - - - - - True - False - 10 - 10 - end - - - Save - True - True - True - 10 - 2 - 2 - - - - True - True - 0 - - - - - Cancel - True - True - True - 10 - 2 - 2 - - - - True - True - 1 - - - - - True - True - 1 - - - - - False - True - 1 - - - - - - - - - diff --git a/Ryujinx/Ui/UrlHelper.cs b/Ryujinx/Ui/UrlHelper.cs deleted file mode 100644 index 79eacc67..00000000 --- a/Ryujinx/Ui/UrlHelper.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Ryujinx.Common.Logging; -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace Ryujinx.Ui -{ - static class UrlHelper - { - public static void OpenUrl(string url) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Process.Start(new ProcessStartInfo("cmd", $"/c start {url.Replace("&", "^&")}")); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - Process.Start("xdg-open", url); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - Process.Start("open", url); - } - else - { - Logger.Notice.Print(LogClass.Application, $"Cannot open url \"{url}\" on this platform!"); - } - } - } -} diff --git a/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs new file mode 100644 index 00000000..4b903d6c --- /dev/null +++ b/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs @@ -0,0 +1,198 @@ +using Gtk; + +namespace Ryujinx.Ui.Widgets +{ + public partial class GameTableContextMenu : Menu + { + private MenuItem _openSaveUserDirMenuItem; + private MenuItem _openSaveDeviceDirMenuItem; + private MenuItem _openSaveBcatDirMenuItem; + private MenuItem _manageTitleUpdatesMenuItem; + private MenuItem _manageDlcMenuItem; + private MenuItem _openTitleModDirMenuItem; + private Menu _extractSubMenu; + private MenuItem _extractMenuItem; + private MenuItem _extractRomFsMenuItem; + private MenuItem _extractExeFsMenuItem; + private MenuItem _extractLogoMenuItem; + private Menu _manageSubMenu; + private MenuItem _manageCacheMenuItem; + private MenuItem _purgePtcCacheMenuItem; + private MenuItem _purgeShaderCacheMenuItem; + private MenuItem _openPtcDirMenuItem; + private MenuItem _openShaderCacheDirMenuItem; + + private void InitializeComponent() + { + // + // _openSaveUserDirMenuItem + // + _openSaveUserDirMenuItem = new MenuItem("Open User Save Directory") + { + TooltipText = "Open the directory which contains Application's User Saves." + }; + _openSaveUserDirMenuItem.Activated += OpenSaveUserDir_Clicked; + + // + // _openSaveDeviceDirMenuItem + // + _openSaveDeviceDirMenuItem = new MenuItem("Open Device Save Directory") + { + TooltipText = "Open the directory which contains Application's Device Saves." + }; + _openSaveDeviceDirMenuItem.Activated += OpenSaveDeviceDir_Clicked; + + // + // _openSaveBcatDirMenuItem + // + _openSaveBcatDirMenuItem = new MenuItem("Open BCAT Save Directory") + { + TooltipText = "Open the directory which contains Application's BCAT Saves." + }; + _openSaveBcatDirMenuItem.Activated += OpenSaveBcatDir_Clicked; + + // + // _manageTitleUpdatesMenuItem + // + _manageTitleUpdatesMenuItem = new MenuItem("Manage Title Updates") + { + TooltipText = "Open the Title Update management window" + }; + _manageTitleUpdatesMenuItem.Activated += ManageTitleUpdates_Clicked; + + // + // _manageDlcMenuItem + // + _manageDlcMenuItem = new MenuItem("Manage DLC") + { + TooltipText = "Open the DLC management window" + }; + _manageDlcMenuItem.Activated += ManageDlc_Clicked; + + // + // _openTitleModDirMenuItem + // + _openTitleModDirMenuItem = new MenuItem("Open Mods Directory") + { + TooltipText = "Open the directory which contains Application's Mods." + }; + _openTitleModDirMenuItem.Activated += OpenTitleModDir_Clicked; + + // + // _extractSubMenu + // + _extractSubMenu = new Menu(); + + // + // _extractMenuItem + // + _extractMenuItem = new MenuItem("Extract Data") + { + Submenu = _extractSubMenu + }; + + // + // _extractRomFsMenuItem + // + _extractRomFsMenuItem = new MenuItem("RomFS") + { + TooltipText = "Extract the RomFS section from Application's current config (including updates)." + }; + _extractRomFsMenuItem.Activated += ExtractRomFs_Clicked; + + // + // _extractExeFsMenuItem + // + _extractExeFsMenuItem = new MenuItem("ExeFS") + { + TooltipText = "Extract the ExeFS section from Application's current config (including updates)." + }; + _extractExeFsMenuItem.Activated += ExtractExeFs_Clicked; + + // + // _extractLogoMenuItem + // + _extractLogoMenuItem = new MenuItem("Logo") + { + TooltipText = "Extract the Logo section from Application's current config (including updates)." + }; + _extractLogoMenuItem.Activated += ExtractLogo_Clicked; + + // + // _manageSubMenu + // + _manageSubMenu = new Menu(); + + // + // _manageCacheMenuItem + // + _manageCacheMenuItem = new MenuItem("Cache Management") + { + Submenu = _manageSubMenu + }; + + // + // _purgePtcCacheMenuItem + // + _purgePtcCacheMenuItem = new MenuItem("Purge PPTC Cache") + { + TooltipText = "Delete the Application's PPTC cache." + }; + _purgePtcCacheMenuItem.Activated += PurgePtcCache_Clicked; + + // + // _purgeShaderCacheMenuItem + // + _purgeShaderCacheMenuItem = new MenuItem("Purge Shader Cache") + { + TooltipText = "Delete the Application's shader cache." + }; + _purgeShaderCacheMenuItem.Activated += PurgeShaderCache_Clicked; + + // + // _openPtcDirMenuItem + // + _openPtcDirMenuItem = new MenuItem("Open PPTC Directory") + { + TooltipText = "Open the directory which contains the Application's PPTC cache." + }; + _openPtcDirMenuItem.Activated += OpenPtcDir_Clicked; + + // + // _openShaderCacheDirMenuItem + // + _openShaderCacheDirMenuItem = new MenuItem("Open Shader Cache Directory") + { + TooltipText = "Open the directory which contains the Application's shader cache." + }; + _openShaderCacheDirMenuItem.Activated += OpenShaderCacheDir_Clicked; + + ShowComponent(); + } + + private void ShowComponent() + { + _extractSubMenu.Append(_extractExeFsMenuItem); + _extractSubMenu.Append(_extractRomFsMenuItem); + _extractSubMenu.Append(_extractLogoMenuItem); + + _manageSubMenu.Append(_purgePtcCacheMenuItem); + _manageSubMenu.Append(_purgeShaderCacheMenuItem); + _manageSubMenu.Append(_openPtcDirMenuItem); + _manageSubMenu.Append(_openShaderCacheDirMenuItem); + + Add(_openSaveUserDirMenuItem); + Add(_openSaveDeviceDirMenuItem); + Add(_openSaveBcatDirMenuItem); + Add(new SeparatorMenuItem()); + Add(_manageTitleUpdatesMenuItem); + Add(_manageDlcMenuItem); + Add(_openTitleModDirMenuItem); + Add(new SeparatorMenuItem()); + Add(_manageCacheMenuItem); + Add(_extractMenuItem); + + ShowAll(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/Ryujinx/Ui/Widgets/GameTableContextMenu.cs new file mode 100644 index 00000000..5ee8baa3 --- /dev/null +++ b/Ryujinx/Ui/Widgets/GameTableContextMenu.cs @@ -0,0 +1,579 @@ +using Gtk; +using LibHac; +using LibHac.Account; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.Fs.Shim; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; +using LibHac.Ncm; +using LibHac.Ns; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.Ui.Helper; +using Ryujinx.Ui.Windows; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; + +using static LibHac.Fs.ApplicationSaveDataManagement; + +namespace Ryujinx.Ui.Widgets +{ + public partial class GameTableContextMenu : Menu + { + private readonly MainWindow _parent; + private readonly VirtualFileSystem _virtualFileSystem; + private readonly BlitStruct _controlData; + + private readonly string _titleFilePath; + private readonly string _titleName; + private readonly string _titleIdText; + private readonly ulong _titleId; + + private MessageDialog _dialog; + private bool _cancel; + + public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleFilePath, string titleName, string titleId, BlitStruct controlData) + { + _parent = parent; + + InitializeComponent(); + + _virtualFileSystem = virtualFileSystem; + _titleFilePath = titleFilePath; + _titleName = titleName; + _titleIdText = titleId; + _controlData = controlData; + + if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId)) + { + GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id"); + + return; + } + + _openSaveUserDirMenuItem.Sensitive = !Utilities.IsEmpty(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0; + _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsEmpty(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0; + _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsEmpty(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0; + + string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower(); + bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci"; + + _extractRomFsMenuItem.Sensitive = hasNca; + _extractExeFsMenuItem.Sensitive = hasNca; + _extractLogoMenuItem.Sensitive = hasNca; + + PopupAtPointer(null); + } + + private bool TryFindSaveData(string titleName, ulong titleId, BlitStruct controlHolder, SaveDataFilter filter, out ulong saveDataId) + { + saveDataId = default; + + Result result = _virtualFileSystem.FsClient.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, ref filter); + + if (ResultFs.TargetNotFound.Includes(result)) + { + // Savedata was not found. Ask the user if they want to create it + using MessageDialog messageDialog = new MessageDialog(null, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, null) + { + Title = "Ryujinx", + Text = $"There is no savedata for {titleName} [{titleId:x16}]", + SecondaryText = "Would you like to create savedata for this game?", + WindowPosition = WindowPosition.Center + }; + + if (messageDialog.Run() != (int)ResponseType.Yes) + { + return false; + } + + ref ApplicationControlProperty control = ref controlHolder.Value; + + if (Utilities.IsEmpty(controlHolder.ByteSpan)) + { + // If the current application doesn't have a loaded control property, create a dummy one + // and set the savedata sizes so a user savedata will be created. + control = ref new BlitStruct(1).Value; + + // The set sizes don't actually matter as long as they're non-zero because we use directory savedata. + control.UserAccountSaveDataSize = 0x4000; + control.UserAccountSaveDataJournalSize = 0x4000; + + Logger.Warning?.Print(LogClass.Application, "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games."); + } + + Uid user = new Uid(1, 0); // TODO: Remove Hardcoded value. + + result = EnsureApplicationSaveData(_virtualFileSystem.FsClient, out _, new LibHac.Ncm.ApplicationId(titleId), ref control, ref user); + + if (result.IsFailure()) + { + GtkDialog.CreateErrorDialog($"There was an error creating the specified savedata: {result.ToStringWithName()}"); + + return false; + } + + // Try to find the savedata again after creating it + result = _virtualFileSystem.FsClient.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, ref filter); + } + + if (result.IsSuccess()) + { + saveDataId = saveDataInfo.SaveDataId; + + return true; + } + + GtkDialog.CreateErrorDialog($"There was an error finding the specified savedata: {result.ToStringWithName()}"); + + return false; + } + + private void OpenSaveDir(SaveDataFilter saveDataFilter) + { + saveDataFilter.SetProgramId(new ProgramId(_titleId)); + + if (!TryFindSaveData(_titleName, _titleId, _controlData, saveDataFilter, out ulong saveDataId)) + { + return; + } + + string saveRootPath = System.IO.Path.Combine(_virtualFileSystem.GetNandPath(), $"user/save/{saveDataId:x16}"); + + if (!Directory.Exists(saveRootPath)) + { + // Inconsistent state. Create the directory + Directory.CreateDirectory(saveRootPath); + } + + string committedPath = System.IO.Path.Combine(saveRootPath, "0"); + string workingPath = System.IO.Path.Combine(saveRootPath, "1"); + + // If the committed directory exists, that path will be loaded the next time the savedata is mounted + if (Directory.Exists(committedPath)) + { + OpenHelper.OpenFolder(committedPath); + } + else + { + // If the working directory exists and the committed directory doesn't, + // the working directory will be loaded the next time the savedata is mounted + if (!Directory.Exists(workingPath)) + { + Directory.CreateDirectory(workingPath); + } + + OpenHelper.OpenFolder(workingPath); + } + } + + private void ExtractSection(NcaSectionType ncaSectionType, int programIndex = 0) + { + FileChooserDialog fileChooser = new FileChooserDialog("Choose the folder to extract into", null, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Extract", ResponseType.Accept); + fileChooser.SetPosition(WindowPosition.Center); + + ResponseType response = (ResponseType)fileChooser.Run(); + string destination = fileChooser.Filename; + + fileChooser.Dispose(); + + if (response == ResponseType.Accept) + { + Thread extractorThread = new Thread(() => + { + Gtk.Application.Invoke(delegate + { + _dialog = new MessageDialog(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Cancel, null) + { + Title = "Ryujinx - NCA Section Extractor", + SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...", + WindowPosition = WindowPosition.Center + }; + + int dialogResponse = _dialog.Run(); + if (dialogResponse == (int)ResponseType.Cancel || dialogResponse == (int)ResponseType.DeleteEvent) + { + _cancel = true; + _dialog.Dispose(); + } + }); + + using (FileStream file = new FileStream(_titleFilePath, FileMode.Open, FileAccess.Read)) + { + Nca mainNca = null; + Nca patchNca = null; + + if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") || + (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") || + (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci")) + { + PartitionFileSystem pfs; + + if (System.IO.Path.GetExtension(_titleFilePath) == ".xci") + { + Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage()); + + pfs = xci.OpenPartition(XciPartitionType.Secure); + } + else + { + pfs = new PartitionFileSystem(file.AsStorage()); + } + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.AsStorage()); + + if (nca.Header.ContentType == NcaContentType.Program) + { + int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program); + + if (nca.Header.GetFsHeader(dataIndex).IsPatchSection()) + { + patchNca = nca; + } + else + { + mainNca = nca; + } + } + } + } + else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca") + { + mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage()); + } + + if (mainNca == null) + { + Logger.Error?.Print(LogClass.Application, "Extraction failed. The main NCA was not present in the selected file."); + + Gtk.Application.Invoke(delegate + { + GtkDialog.CreateErrorDialog("Extraction failed. The main NCA was not present in the selected file."); + }); + + return; + } + + (Nca updatePatchNca, _) = ApplicationLoader.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _); + + if (updatePatchNca != null) + { + patchNca = updatePatchNca; + } + + int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType); + + IFileSystem ncaFileSystem = patchNca != null ? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid) + : mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid); + + FileSystemClient fsClient = _virtualFileSystem.FsClient; + + string source = DateTime.Now.ToFileTime().ToString()[10..]; + string output = DateTime.Now.ToFileTime().ToString()[10..]; + + fsClient.Register(source.ToU8Span(), ncaFileSystem); + fsClient.Register(output.ToU8Span(), new LocalFileSystem(destination)); + + (Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/"); + + if (!canceled) + { + if (resultCode.Value.IsFailure()) + { + Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}"); + + Gtk.Application.Invoke(delegate + { + _dialog?.Dispose(); + + GtkDialog.CreateErrorDialog("Extraction failed. Read the log file for further information."); + }); + } + else if (resultCode.Value.IsSuccess()) + { + Gtk.Application.Invoke(delegate + { + _dialog?.Dispose(); + + MessageDialog dialog = new MessageDialog(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null) + { + Title = "Ryujinx - NCA Section Extractor", + SecondaryText = "Extraction has completed successfully.", + WindowPosition = WindowPosition.Center + }; + + dialog.Run(); + dialog.Dispose(); + }); + } + } + + fsClient.Unmount(source.ToU8Span()); + fsClient.Unmount(output.ToU8Span()); + } + }); + + extractorThread.Name = "GUI.NcaSectionExtractorThread"; + extractorThread.IsBackground = true; + extractorThread.Start(); + } + } + + private (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath) + { + Result rc = fs.OpenDirectory(out DirectoryHandle sourceHandle, sourcePath.ToU8Span(), OpenDirectoryMode.All); + if (rc.IsFailure()) return (rc, false); + + using (sourceHandle) + { + foreach (DirectoryEntryEx entry in fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default)) + { + if (_cancel) + { + return (null, true); + } + + string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); + string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); + + if (entry.Type == DirectoryEntryType.Directory) + { + fs.EnsureDirectoryExists(subDstPath); + + (Result? result, bool canceled) = CopyDirectory(fs, subSrcPath, subDstPath); + if (canceled || result.Value.IsFailure()) + { + return (result, canceled); + } + } + + if (entry.Type == DirectoryEntryType.File) + { + fs.CreateOrOverwriteFile(subDstPath, entry.Size); + + rc = CopyFile(fs, subSrcPath, subDstPath); + if (rc.IsFailure()) return (rc, false); + } + } + } + + return (Result.Success, false); + } + + public Result CopyFile(FileSystemClient fs, string sourcePath, string destPath) + { + Result rc = fs.OpenFile(out FileHandle sourceHandle, sourcePath.ToU8Span(), OpenMode.Read); + if (rc.IsFailure()) return rc; + + using (sourceHandle) + { + rc = fs.OpenFile(out FileHandle destHandle, destPath.ToU8Span(), OpenMode.Write | OpenMode.AllowAppend); + if (rc.IsFailure()) return rc; + + using (destHandle) + { + const int maxBufferSize = 1024 * 1024; + + rc = fs.GetFileSize(out long fileSize, sourceHandle); + if (rc.IsFailure()) return rc; + + int bufferSize = (int)Math.Min(maxBufferSize, fileSize); + + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + try + { + for (long offset = 0; offset < fileSize; offset += bufferSize) + { + int toRead = (int)Math.Min(fileSize - offset, bufferSize); + Span buf = buffer.AsSpan(0, toRead); + + rc = fs.ReadFile(out long _, sourceHandle, offset, buf); + if (rc.IsFailure()) return rc; + + rc = fs.WriteFile(destHandle, offset, buf); + if (rc.IsFailure()) return rc; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + rc = fs.FlushFile(destHandle); + if (rc.IsFailure()) return rc; + } + } + + return Result.Success; + } + + // + // Events + // + private void OpenSaveUserDir_Clicked(object sender, EventArgs args) + { + SaveDataFilter saveDataFilter = new SaveDataFilter(); + saveDataFilter.SetUserId(new UserId(1, 0)); // TODO: Remove Hardcoded value. + + OpenSaveDir(saveDataFilter); + } + + private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args) + { + SaveDataFilter saveDataFilter = new SaveDataFilter(); + saveDataFilter.SetSaveDataType(SaveDataType.Device); + + OpenSaveDir(saveDataFilter); + } + + private void OpenSaveBcatDir_Clicked(object sender, EventArgs args) + { + SaveDataFilter saveDataFilter = new SaveDataFilter(); + saveDataFilter.SetSaveDataType(SaveDataType.Bcat); + + OpenSaveDir(saveDataFilter); + } + + private void ManageTitleUpdates_Clicked(object sender, EventArgs args) + { + new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show(); + } + + private void ManageDlc_Clicked(object sender, EventArgs args) + { + new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show(); + } + + private void OpenTitleModDir_Clicked(object sender, EventArgs args) + { + string modsBasePath = _virtualFileSystem.ModLoader.GetModsBasePath(); + string titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, _titleIdText); + + OpenHelper.OpenFolder(titleModsPath); + } + + private void ExtractRomFs_Clicked(object sender, EventArgs args) + { + ExtractSection(NcaSectionType.Data); + } + + private void ExtractExeFs_Clicked(object sender, EventArgs args) + { + ExtractSection(NcaSectionType.Code); + } + + private void ExtractLogo_Clicked(object sender, EventArgs args) + { + ExtractSection(NcaSectionType.Logo); + } + + private void OpenPtcDir_Clicked(object sender, EventArgs args) + { + string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu"); + + string mainPath = System.IO.Path.Combine(ptcDir, "0"); + string backupPath = System.IO.Path.Combine(ptcDir, "1"); + + if (!Directory.Exists(ptcDir)) + { + Directory.CreateDirectory(ptcDir); + Directory.CreateDirectory(mainPath); + Directory.CreateDirectory(backupPath); + } + + OpenHelper.OpenFolder(ptcDir); + } + + private void OpenShaderCacheDir_Clicked(object sender, EventArgs args) + { + string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"); + + if (!Directory.Exists(shaderCacheDir)) + { + Directory.CreateDirectory(shaderCacheDir); + } + + OpenHelper.OpenFolder(shaderCacheDir); + } + + private void PurgePtcCache_Clicked(object sender, EventArgs args) + { + DirectoryInfo mainDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0")); + DirectoryInfo backupDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1")); + + MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the PPTC cache for :\n\n{_titleName}\n\nAre you sure you want to proceed?"); + + List cacheFiles = new List(); + + if (mainDir.Exists) + { + cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache")); + } + + if (backupDir.Exists) + { + cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache")); + } + + if (cacheFiles.Count > 0 && warningDialog.Run() == (int)ResponseType.Yes) + { + foreach (FileInfo file in cacheFiles) + { + try + { + file.Delete(); + } + catch(Exception e) + { + GtkDialog.CreateErrorDialog($"Error purging PPTC cache {file.Name}: {e}"); + } + } + } + + warningDialog.Dispose(); + } + + private void PurgeShaderCache_Clicked(object sender, EventArgs args) + { + DirectoryInfo shaderCacheDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader")); + + MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_titleName}\n\nAre you sure you want to proceed?"); + + List cacheDirectory = new List(); + + if (shaderCacheDir.Exists) + { + cacheDirectory.AddRange(shaderCacheDir.EnumerateDirectories("*")); + } + + if (cacheDirectory.Count > 0 && warningDialog.Run() == (int)ResponseType.Yes) + { + foreach (DirectoryInfo directory in cacheDirectory) + { + try + { + directory.Delete(true); + } + catch (Exception e) + { + GtkDialog.CreateErrorDialog($"Error purging shader cache {directory.Name}: {e}"); + } + } + } + + warningDialog.Dispose(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Widgets/GtkDialog.cs b/Ryujinx/Ui/Widgets/GtkDialog.cs new file mode 100644 index 00000000..e603383a --- /dev/null +++ b/Ryujinx/Ui/Widgets/GtkDialog.cs @@ -0,0 +1,82 @@ +using Gtk; +using Ryujinx.Common.Logging; + +namespace Ryujinx.Ui.Widgets +{ + internal class GtkDialog : MessageDialog + { + private static bool _isChoiceDialogOpen; + + private GtkDialog(string title, string mainText, string secondaryText, MessageType messageType = MessageType.Other, ButtonsType buttonsType = ButtonsType.Ok) + : base(null, DialogFlags.Modal, messageType, buttonsType, null) + { + Title = title; + Text = mainText; + SecondaryText = secondaryText; + WindowPosition = WindowPosition.Center; + SecondaryUseMarkup = true; + + Response += GtkDialog_Response; + + SetSizeRequest(200, 20); + } + + private void GtkDialog_Response(object sender, ResponseArgs args) + { + Dispose(); + } + + internal static void CreateInfoDialog(string mainText, string secondaryText) + { + new GtkDialog("Ryujinx - Info", mainText, secondaryText, MessageType.Info).Run(); + } + + internal static void CreateUpdaterInfoDialog(string mainText, string secondaryText) + { + new GtkDialog("Ryujinx - Updater", mainText, secondaryText, MessageType.Info).Run(); + } + + internal static MessageDialog CreateWaitingDialog(string mainText, string secondaryText) + { + return new GtkDialog("Ryujinx - Waiting", mainText, secondaryText, MessageType.Info, ButtonsType.None); + } + + internal static void CreateWarningDialog(string mainText, string secondaryText) + { + new GtkDialog("Ryujinx - Warning", mainText, secondaryText, MessageType.Warning).Run(); + } + + internal static void CreateErrorDialog(string errorMessage) + { + Logger.Error?.Print(LogClass.Application, errorMessage); + + new GtkDialog("Ryujinx - Error", "Ryujinx has encountered an error", errorMessage, MessageType.Error).Run(); + } + + internal static MessageDialog CreateConfirmationDialog(string mainText, string secondaryText = "") + { + return new GtkDialog("Ryujinx - Confirmation", mainText, secondaryText, MessageType.Question, ButtonsType.YesNo); + } + + internal static bool CreateChoiceDialog(string title, string mainText, string secondaryText) + { + if (_isChoiceDialogOpen) + { + return false; + } + + _isChoiceDialogOpen = true; + + ResponseType response = (ResponseType)new GtkDialog(title, mainText, secondaryText, MessageType.Question, ButtonsType.YesNo).Run(); + + _isChoiceDialogOpen = false; + + return response == ResponseType.Yes; + } + + internal static bool CreateExitDialog() + { + return CreateChoiceDialog("Ryujinx - Exit", "Are you sure you want to stop emulation?", "All unsaved data will be lost!"); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Widgets/ProfileDialog.cs b/Ryujinx/Ui/Widgets/ProfileDialog.cs new file mode 100644 index 00000000..86667572 --- /dev/null +++ b/Ryujinx/Ui/Widgets/ProfileDialog.cs @@ -0,0 +1,55 @@ +using Gtk; +using System; + +using GUI = Gtk.Builder.ObjectAttribute; + +namespace Ryujinx.Ui.Widgets +{ + public class ProfileDialog : Dialog + { + public string FileName { get; private set; } + +#pragma warning disable CS0649, IDE0044 + [GUI] Entry _profileEntry; + [GUI] Label _errorMessage; +#pragma warning restore CS0649, IDE0044 + + public ProfileDialog() : this(new Builder("Ryujinx.Ui.Widgets.ProfileDialog.glade")) { } + + private ProfileDialog(Builder builder) : base(builder.GetObject("_profileDialog").Handle) + { + builder.Autoconnect(this); + } + + private void OkToggle_Activated(object sender, EventArgs args) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + bool validFileName = true; + + foreach (char invalidChar in System.IO.Path.GetInvalidFileNameChars()) + { + if (_profileEntry.Text.Contains(invalidChar)) + { + validFileName = false; + } + } + + if (validFileName && !string.IsNullOrEmpty(_profileEntry.Text)) + { + FileName = $"{_profileEntry.Text}.json"; + + Respond(ResponseType.Ok); + } + else + { + _errorMessage.Text = "The file name contains invalid characters. Please try again."; + } + } + + private void CancelToggle_Activated(object sender, EventArgs args) + { + Respond(ResponseType.Cancel); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Widgets/ProfileDialog.glade b/Ryujinx/Ui/Widgets/ProfileDialog.glade new file mode 100644 index 00000000..adaf6608 --- /dev/null +++ b/Ryujinx/Ui/Widgets/ProfileDialog.glade @@ -0,0 +1,124 @@ + + + + + + False + Ryujinx - Profile Dialog + True + center + 400 + dialog + + + + + + False + vertical + 2 + + + False + end + + + OK + True + True + True + + + + False + True + 0 + + + + + Cancel + True + True + True + + + + False + True + 5 + 1 + + + + + False + False + 0 + + + + + True + False + vertical + + + True + False + 10 + 10 + 20 + 10 + Enter a name for the new profile: + + + True + True + 0 + + + + + True + True + 20 + 20 + 20 + + + True + True + 1 + + + + + True + False + start + 20 + 10 + 10 + 10 + + + + + + False + True + 2 + + + + + True + True + 1 + + + + + + diff --git a/Ryujinx/Ui/Widgets/UserError.cs b/Ryujinx/Ui/Widgets/UserError.cs new file mode 100644 index 00000000..08695571 --- /dev/null +++ b/Ryujinx/Ui/Widgets/UserError.cs @@ -0,0 +1,39 @@ +namespace Ryujinx.Ui.Widgets +{ + /// + /// Represent a common error that could be reported to the user by the emulator. + /// + public enum UserError + { + /// + /// No error to report. + /// + Success = 0x0, + + /// + /// No keys are present. + /// + NoKeys = 0x1, + + /// + /// No firmware is installed. + /// + NoFirmware = 0x2, + + /// + /// Firmware parsing failed. + /// + /// Most likely related to keys. + FirmwareParsingFailed = 0x3, + + /// + /// No application was found at the given path. + /// + ApplicationNotFound = 0x4, + + /// + /// An unknown error. + /// + Unknown = 0xDEAD + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Widgets/UserErrorDialog.cs b/Ryujinx/Ui/Widgets/UserErrorDialog.cs new file mode 100644 index 00000000..095b88a3 --- /dev/null +++ b/Ryujinx/Ui/Widgets/UserErrorDialog.cs @@ -0,0 +1,122 @@ +using Gtk; +using Ryujinx.Ui.Helper; + +namespace Ryujinx.Ui.Widgets +{ + internal class UserErrorDialog : MessageDialog + { + private const string SetupGuideUrl = "https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide"; + private const int OkResponseId = 0; + private const int SetupGuideResponseId = 1; + + private readonly UserError _userError; + + private UserErrorDialog(UserError error) : base(null, DialogFlags.Modal, MessageType.Error, ButtonsType.None, null) + { + _userError = error; + + WindowPosition = WindowPosition.Center; + SecondaryUseMarkup = true; + + Response += UserErrorDialog_Response; + + SetSizeRequest(120, 50); + + AddButton("OK", OkResponseId); + + bool isInSetupGuide = IsCoveredBySetupGuide(error); + + if (isInSetupGuide) + { + AddButton("Open the Setup Guide", SetupGuideResponseId); + } + + string errorCode = GetErrorCode(error); + + SecondaryUseMarkup = true; + + Title = $"Ryujinx error ({errorCode})"; + Text = $"{errorCode}: {GetErrorTitle(error)}"; + SecondaryText = GetErrorDescription(error); + + if (isInSetupGuide) + { + SecondaryText += "\nFor more information on how to fix this error, follow our Setup Guide."; + } + } + + private string GetErrorCode(UserError error) + { + return $"RYU-{(uint)error:X4}"; + } + + private string GetErrorTitle(UserError error) + { + return error switch + { + UserError.NoKeys => "Keys not found", + UserError.NoFirmware => "Firmware not found", + UserError.FirmwareParsingFailed => "Firmware parsing error", + UserError.ApplicationNotFound => "Application not found", + UserError.Unknown => "Unknown error", + _ => "Undefined error", + }; + } + + private string GetErrorDescription(UserError error) + { + return error switch + { + UserError.NoKeys => "Ryujinx was unable to find your 'prod.keys' file", + UserError.NoFirmware => "Ryujinx was unable to find any firmwares installed", + UserError.FirmwareParsingFailed => "Ryujinx was unable to parse the provided firmware. This is usually caused by outdated keys.", + UserError.ApplicationNotFound => "Ryujinx couldn't find a valid application at the given path.", + UserError.Unknown => "An unknown error occured!", + _ => "An undefined error occured! This shouldn't happen, please contact a dev!", + }; + } + + private static bool IsCoveredBySetupGuide(UserError error) + { + return error switch + { + UserError.NoKeys or + UserError.NoFirmware or + UserError.FirmwareParsingFailed => true, + _ => false, + }; + } + + private static string GetSetupGuideUrl(UserError error) + { + if (!IsCoveredBySetupGuide(error)) + { + return null; + } + + return error switch + { + UserError.NoKeys => SetupGuideUrl + "#initial-setup---placement-of-prodkeys", + UserError.NoFirmware => SetupGuideUrl + "#initial-setup-continued---installation-of-firmware", + _ => SetupGuideUrl, + }; + } + + private void UserErrorDialog_Response(object sender, ResponseArgs args) + { + int responseId = (int)args.ResponseId; + + if (responseId == SetupGuideResponseId) + { + OpenHelper.OpenUrl(GetSetupGuideUrl(_userError)); + } + + Dispose(); + } + + public static void CreateUserErrorDialog(UserError error) + { + new UserErrorDialog(error).Run(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Windows/AboutWindow.Designer.cs b/Ryujinx/Ui/Windows/AboutWindow.Designer.cs new file mode 100644 index 00000000..99ce9333 --- /dev/null +++ b/Ryujinx/Ui/Windows/AboutWindow.Designer.cs @@ -0,0 +1,467 @@ +using Gtk; +using Pango; +using System.Reflection; + +namespace Ryujinx.Ui.Windows +{ + public partial class AboutWindow : Window + { + private Box _mainBox; + private Box _leftBox; + private Box _logoBox; + private Image _ryujinxLogo; + private Box _logoTextBox; + private Label _ryujinxLabel; + private Label _ryujinxPhoneticLabel; + private EventBox _ryujinxLink; + private Label _ryujinxLinkLabel; + private Label _versionLabel; + private Label _disclaimerLabel; + private Box _socialBox; + private EventBox _patreonEventBox; + private Box _patreonBox; + private Image _patreonLogo; + private Label _patreonLabel; + private EventBox _githubEventBox; + private Box _githubBox; + private Image _githubLogo; + private Label _githubLabel; + private Box _discordBox; + private EventBox _discordEventBox; + private Image _discordLogo; + private Label _discordLabel; + private EventBox _twitterEventBox; + private Box _twitterBox; + private Image _twitterLogo; + private Label _twitterLabel; + private Separator _separator; + private Box _rightBox; + private Label _aboutLabel; + private Label _aboutDescriptionLabel; + private Label _createdByLabel; + private TextView _createdByText; + private EventBox _contributorsEventBox; + private Label _contributorsLinkLabel; + private Label _patreonNamesLabel; + private ScrolledWindow _patreonNamesScrolled; + private TextView _patreonNamesText; + + private void InitializeComponent() + { + +#pragma warning disable CS0612 + + // + // AboutWindow + // + CanFocus = false; + Resizable = false; + Modal = true; + WindowPosition = WindowPosition.Center; + DefaultWidth = 800; + DefaultHeight = 450; + TypeHint = Gdk.WindowTypeHint.Dialog; + + // + // _mainBox + // + _mainBox = new Box(Orientation.Horizontal, 0); + + // + // _leftBox + // + _leftBox = new Box(Orientation.Vertical, 0) + { + Margin = 15, + MarginLeft = 30, + MarginRight = 0 + }; + + // + // _logoBox + // + _logoBox = new Box(Orientation.Horizontal, 0); + + // + // _ryujinxLogo + // + _ryujinxLogo = new Image(new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Ryujinx.png", 100, 100)) + { + Margin = 10, + MarginLeft = 15 + }; + + // + // _logoTextBox + // + _logoTextBox = new Box(Orientation.Vertical, 0); + + // + // _ryujinxLabel + // + _ryujinxLabel = new Label("Ryujinx") + { + MarginTop = 15, + Justify = Justification.Center, + Attributes = new AttrList() + }; + _ryujinxLabel.Attributes.Insert(new Pango.AttrScale(2.7f)); + + // + // _ryujinxPhoneticLabel + // + _ryujinxPhoneticLabel = new Label("(REE-YOU-JI-NX)") + { + Justify = Justification.Center + }; + + // + // _ryujinxLink + // + _ryujinxLink = new EventBox() + { + Margin = 5 + }; + _ryujinxLink.ButtonPressEvent += RyujinxButton_Pressed; + + // + // _ryujinxLinkLabel + // + _ryujinxLinkLabel = new Label("www.ryujinx.org") + { + TooltipText = "Click to open the Ryujinx website in your default browser.", + Justify = Justification.Center, + Attributes = new AttrList() + }; + _ryujinxLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _versionLabel + // + _versionLabel = new Label(Program.Version) + { + Expand = true, + Justify = Justification.Center, + Margin = 5 + }; + + // + // _disclaimerLabel + // + _disclaimerLabel = new Label("Ryujinx is not affiliated with Nintendo™,\nor any of its partners, in any way.") + { + Expand = true, + Justify = Justification.Center, + Margin = 5, + Attributes = new AttrList() + }; + _disclaimerLabel.Attributes.Insert(new Pango.AttrScale(0.8f)); + + // + // _socialBox + // + _socialBox = new Box(Orientation.Horizontal, 0) + { + Margin = 25, + MarginBottom = 10 + }; + + // + // _patreonEventBox + // + _patreonEventBox = new EventBox() + { + TooltipText = "Click to open the Ryujinx Patreon page in your default browser." + }; + _patreonEventBox.ButtonPressEvent += PatreonButton_Pressed; + + // + // _patreonBox + // + _patreonBox = new Box(Orientation.Vertical, 0); + + // + // _patreonLogo + // + _patreonLogo = new Image(new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Patreon.png", 30, 30)) + { + Margin = 10 + }; + + // + // _patreonLabel + // + _patreonLabel = new Label("Patreon") + { + Justify = Justification.Center + }; + + // + // _githubEventBox + // + _githubEventBox = new EventBox() + { + TooltipText = "Click to open the Ryujinx GitHub page in your default browser." + }; + _githubEventBox.ButtonPressEvent += GitHubButton_Pressed; + + // + // _githubBox + // + _githubBox = new Box(Orientation.Vertical, 0); + + // + // _githubLogo + // + _githubLogo = new Image(new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_GitHub.png", 30, 30)) + { + Margin = 10 + }; + + // + // _githubLabel + // + _githubLabel = new Label("GitHub") + { + Justify = Justification.Center + }; + + // + // _discordBox + // + _discordBox = new Box(Orientation.Vertical, 0); + + // + // _discordEventBox + // + _discordEventBox = new EventBox() + { + TooltipText = "Click to open an invite to the Ryujinx Discord server in your default browser." + }; + _discordEventBox.ButtonPressEvent += DiscordButton_Pressed; + + // + // _discordLogo + // + _discordLogo = new Image(new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Discord.png", 30, 30)) + { + Margin = 10 + }; + + // + // _discordLabel + // + _discordLabel = new Label("Discord") + { + Justify = Justification.Center + }; + + // + // _twitterEventBox + // + _twitterEventBox = new EventBox() + { + TooltipText = "Click to open the Ryujinx Twitter page in your default browser." + }; + _twitterEventBox.ButtonPressEvent += TwitterButton_Pressed; + + // + // _twitterBox + // + _twitterBox = new Box(Orientation.Vertical, 0); + + // + // _twitterLogo + // + _twitterLogo = new Image(new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Twitter.png", 30, 30)) + { + Margin = 10 + }; + + // + // _twitterLabel + // + _twitterLabel = new Label("Twitter") + { + Justify = Justification.Center + }; + + // + // _separator + // + _separator = new Separator(Orientation.Vertical) + { + Margin = 15 + }; + + // + // _rightBox + // + _rightBox = new Box(Orientation.Vertical, 0) + { + Margin = 15, + MarginTop = 40 + }; + + // + // _aboutLabel + // + _aboutLabel = new Label("About :") + { + Halign = Align.Start, + Attributes = new AttrList() + }; + _aboutLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); + _aboutLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _aboutDescriptionLabel + // + _aboutDescriptionLabel = new Label("Ryujinx is an emulator for the Nintendo Switch™.\n" + + "Please support us on Patreon.\n" + + "Get all the latest news on our Twitter or Discord.\n" + + "Developers interested in contributing can find out more on our GitHub or Discord.") + { + Margin = 15, + Halign = Align.Start + }; + + // + // _createdByLabel + // + _createdByLabel = new Label("Maintained by :") + { + Halign = Align.Start, + Attributes = new AttrList() + }; + _createdByLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); + _createdByLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _createdByText + // + _createdByText = new TextView() + { + WrapMode = Gtk.WrapMode.Word, + Editable = false, + CursorVisible = false, + Margin = 15, + MarginRight = 30 + }; + _createdByText.Buffer.Text = "gdkchan, Ac_K, Thog, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, Xpl0itR, GoffyDude, »jD« and more..."; + + // + // _contributorsEventBox + // + _contributorsEventBox = new EventBox(); + _contributorsEventBox.ButtonPressEvent += ContributorsButton_Pressed; + + // + // _contributorsLinkLabel + // + _contributorsLinkLabel = new Label("See All Contributors...") + { + TooltipText = "Click to open the Contributors page in your default browser.", + MarginRight = 30, + Halign = Align.End, + Attributes = new AttrList() + }; + _contributorsLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _patreonNamesLabel + // + _patreonNamesLabel = new Label("Supported on Patreon by :") + { + Halign = Align.Start, + Attributes = new AttrList() + }; + _patreonNamesLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold)); + _patreonNamesLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single)); + + // + // _patreonNamesScrolled + // + _patreonNamesScrolled = new ScrolledWindow() + { + Margin = 15, + MarginRight = 30, + Expand = true, + ShadowType = ShadowType.In + }; + _patreonNamesScrolled.SetPolicy(PolicyType.Never, PolicyType.Automatic); + + // + // _patreonNamesText + // + _patreonNamesText = new TextView() + { + WrapMode = Gtk.WrapMode.Word + }; + _patreonNamesText.Buffer.Text = "Loading..."; + _patreonNamesText.SetProperty("editable", new GLib.Value(false)); + +#pragma warning restore CS0612 + + ShowComponent(); + } + + private void ShowComponent() + { + _logoBox.Add(_ryujinxLogo); + + _ryujinxLink.Add(_ryujinxLinkLabel); + + _logoTextBox.Add(_ryujinxLabel); + _logoTextBox.Add(_ryujinxPhoneticLabel); + _logoTextBox.Add(_ryujinxLink); + + _logoBox.Add(_logoTextBox); + + _patreonBox.Add(_patreonLogo); + _patreonBox.Add(_patreonLabel); + _patreonEventBox.Add(_patreonBox); + + _githubBox.Add(_githubLogo); + _githubBox.Add(_githubLabel); + _githubEventBox.Add(_githubBox); + + _discordBox.Add(_discordLogo); + _discordBox.Add(_discordLabel); + _discordEventBox.Add(_discordBox); + + _twitterBox.Add(_twitterLogo); + _twitterBox.Add(_twitterLabel); + _twitterEventBox.Add(_twitterBox); + + _socialBox.Add(_patreonEventBox); + _socialBox.Add(_githubEventBox); + _socialBox.Add(_discordEventBox); + _socialBox.Add(_twitterEventBox); + + _leftBox.Add(_logoBox); + _leftBox.Add(_versionLabel); + _leftBox.Add(_disclaimerLabel); + _leftBox.Add(_socialBox); + + _contributorsEventBox.Add(_contributorsLinkLabel); + _patreonNamesScrolled.Add(_patreonNamesText); + + _rightBox.Add(_aboutLabel); + _rightBox.Add(_aboutDescriptionLabel); + _rightBox.Add(_createdByLabel); + _rightBox.Add(_createdByText); + _rightBox.Add(_contributorsEventBox); + _rightBox.Add(_patreonNamesLabel); + _rightBox.Add(_patreonNamesScrolled); + + _mainBox.Add(_leftBox); + _mainBox.Add(_separator); + _mainBox.Add(_rightBox); + + Add(_mainBox); + + ShowAll(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Windows/AboutWindow.cs b/Ryujinx/Ui/Windows/AboutWindow.cs new file mode 100644 index 00000000..ab93e41d --- /dev/null +++ b/Ryujinx/Ui/Windows/AboutWindow.cs @@ -0,0 +1,73 @@ +using Gtk; +using Ryujinx.Common.Utilities; +using Ryujinx.Ui.Helper; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Threading.Tasks; + +namespace Ryujinx.Ui.Windows +{ + public partial class AboutWindow : Window + { + public AboutWindow() : base($"Ryujinx {Program.Version} - About") + { + InitializeComponent(); + + _ = DownloadPatronsJson(); + } + + private async Task DownloadPatronsJson() + { + if (!NetworkInterface.GetIsNetworkAvailable()) + { + _patreonNamesText.Buffer.Text = "Connection Error."; + } + + HttpClient httpClient = new HttpClient(); + + try + { + string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/"); + + _patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize(patreonJsonString)); + } + catch + { + _patreonNamesText.Buffer.Text = "API Error."; + } + } + + // + // Events + // + private void RyujinxButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://ryujinx.org"); + } + + private void PatreonButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://www.patreon.com/ryujinx"); + } + + private void GitHubButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx"); + } + + private void DiscordButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://discordapp.com/invite/N2FmfVc"); + } + + private void TwitterButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://twitter.com/RyujinxEmu"); + } + + private void ContributorsButton_Pressed(object sender, ButtonPressEventArgs args) + { + OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a"); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Windows/ControllerWindow.cs b/Ryujinx/Ui/Windows/ControllerWindow.cs new file mode 100644 index 00000000..7b0f7cf8 --- /dev/null +++ b/Ryujinx/Ui/Windows/ControllerWindow.cs @@ -0,0 +1,1027 @@ +using Gtk; +using OpenTK.Input; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Utilities; +using Ryujinx.Configuration; +using Ryujinx.Ui.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text.Json; +using System.Threading; + +using GUI = Gtk.Builder.ObjectAttribute; +using Key = Ryujinx.Configuration.Hid.Key; + +namespace Ryujinx.Ui.Windows +{ + public class ControllerWindow : Window + { + private readonly PlayerIndex _playerIndex; + private readonly InputConfig _inputConfig; + + private bool _isWaitingForInput; + +#pragma warning disable CS0649, IDE0044 + [GUI] Adjustment _controllerDeadzoneLeft; + [GUI] Adjustment _controllerDeadzoneRight; + [GUI] Adjustment _controllerTriggerThreshold; + [GUI] Adjustment _slotNumber; + [GUI] Adjustment _altSlotNumber; + [GUI] Adjustment _sensitivity; + [GUI] Adjustment _gyroDeadzone; + [GUI] CheckButton _enableMotion; + [GUI] CheckButton _mirrorInput; + [GUI] Entry _dsuServerHost; + [GUI] Entry _dsuServerPort; + [GUI] ComboBoxText _inputDevice; + [GUI] ComboBoxText _profile; + [GUI] ToggleButton _refreshInputDevicesButton; + [GUI] Box _settingsBox; + [GUI] Box _altBox; + [GUI] Grid _leftStickKeyboard; + [GUI] Grid _leftStickController; + [GUI] Box _deadZoneLeftBox; + [GUI] Grid _rightStickKeyboard; + [GUI] Grid _rightStickController; + [GUI] Box _deadZoneRightBox; + [GUI] Grid _leftSideTriggerBox; + [GUI] Grid _rightSideTriggerBox; + [GUI] Box _triggerThresholdBox; + [GUI] ComboBoxText _controllerType; + [GUI] ToggleButton _lStickX; + [GUI] CheckButton _invertLStickX; + [GUI] ToggleButton _lStickY; + [GUI] CheckButton _invertLStickY; + [GUI] ToggleButton _lStickUp; + [GUI] ToggleButton _lStickDown; + [GUI] ToggleButton _lStickLeft; + [GUI] ToggleButton _lStickRight; + [GUI] ToggleButton _lStickButton; + [GUI] ToggleButton _dpadUp; + [GUI] ToggleButton _dpadDown; + [GUI] ToggleButton _dpadLeft; + [GUI] ToggleButton _dpadRight; + [GUI] ToggleButton _minus; + [GUI] ToggleButton _l; + [GUI] ToggleButton _zL; + [GUI] ToggleButton _rStickX; + [GUI] CheckButton _invertRStickX; + [GUI] ToggleButton _rStickY; + [GUI] CheckButton _invertRStickY; + [GUI] ToggleButton _rStickUp; + [GUI] ToggleButton _rStickDown; + [GUI] ToggleButton _rStickLeft; + [GUI] ToggleButton _rStickRight; + [GUI] ToggleButton _rStickButton; + [GUI] ToggleButton _a; + [GUI] ToggleButton _b; + [GUI] ToggleButton _x; + [GUI] ToggleButton _y; + [GUI] ToggleButton _plus; + [GUI] ToggleButton _r; + [GUI] ToggleButton _zR; + [GUI] ToggleButton _lSl; + [GUI] ToggleButton _lSr; + [GUI] ToggleButton _rSl; + [GUI] ToggleButton _rSr; + [GUI] Image _controllerImage; +#pragma warning restore CS0649, IDE0044 + + public ControllerWindow(PlayerIndex controllerId) : this(new Builder("Ryujinx.Ui.Windows.ControllerWindow.glade"), controllerId) { } + + private ControllerWindow(Builder builder, PlayerIndex controllerId) : base(builder.GetObject("_controllerWin").Handle) + { + builder.Autoconnect(this); + + _playerIndex = controllerId; + _inputConfig = ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerIndex); + + Title = $"Ryujinx - Controller Settings - {_playerIndex}"; + + if (_playerIndex == PlayerIndex.Handheld) + { + _controllerType.Append(ControllerType.Handheld.ToString(), "Handheld"); + _controllerType.Sensitive = false; + } + else + { + _controllerType.Append(ControllerType.ProController.ToString(), "Pro Controller"); + _controllerType.Append(ControllerType.JoyconPair.ToString(), "Joycon Pair"); + _controllerType.Append(ControllerType.JoyconLeft.ToString(), "Joycon Left"); + _controllerType.Append(ControllerType.JoyconRight.ToString(), "Joycon Right"); + } + + _controllerType.Active = 0; // Set initial value to first in list. + + // Bind Events. + _lStickX.Clicked += Button_Pressed; + _lStickY.Clicked += Button_Pressed; + _lStickUp.Clicked += Button_Pressed; + _lStickDown.Clicked += Button_Pressed; + _lStickLeft.Clicked += Button_Pressed; + _lStickRight.Clicked += Button_Pressed; + _lStickButton.Clicked += Button_Pressed; + _dpadUp.Clicked += Button_Pressed; + _dpadDown.Clicked += Button_Pressed; + _dpadLeft.Clicked += Button_Pressed; + _dpadRight.Clicked += Button_Pressed; + _minus.Clicked += Button_Pressed; + _l.Clicked += Button_Pressed; + _zL.Clicked += Button_Pressed; + _lSl.Clicked += Button_Pressed; + _lSr.Clicked += Button_Pressed; + _rStickX.Clicked += Button_Pressed; + _rStickY.Clicked += Button_Pressed; + _rStickUp.Clicked += Button_Pressed; + _rStickDown.Clicked += Button_Pressed; + _rStickLeft.Clicked += Button_Pressed; + _rStickRight.Clicked += Button_Pressed; + _rStickButton.Clicked += Button_Pressed; + _a.Clicked += Button_Pressed; + _b.Clicked += Button_Pressed; + _x.Clicked += Button_Pressed; + _y.Clicked += Button_Pressed; + _plus.Clicked += Button_Pressed; + _r.Clicked += Button_Pressed; + _zR.Clicked += Button_Pressed; + _rSl.Clicked += Button_Pressed; + _rSr.Clicked += Button_Pressed; + + // Setup current values. + UpdateInputDeviceList(); + SetAvailableOptions(); + + ClearValues(); + if (_inputDevice.ActiveId != null) + { + SetCurrentValues(); + } + } + + private void UpdateInputDeviceList() + { + _inputDevice.RemoveAll(); + _inputDevice.Append("disabled", "Disabled"); + _inputDevice.SetActiveId("disabled"); + + _inputDevice.Append($"keyboard/{KeyboardConfig.AllKeyboardsIndex}", "All keyboards"); + + for (int i = 0; i < 20; i++) + { + if (KeyboardController.GetKeyboardState(i + 1).IsConnected) + _inputDevice.Append($"keyboard/{i + 1}", $"Keyboard/{i + 1}"); + + if (GamePad.GetState(i).IsConnected) + _inputDevice.Append($"controller/{i}", $"Controller/{i} ({GamePad.GetName(i)})"); + } + + switch (_inputConfig) + { + case KeyboardConfig keyboard: + _inputDevice.SetActiveId($"keyboard/{keyboard.Index}"); + break; + case ControllerConfig controller: + _inputDevice.SetActiveId($"controller/{controller.Index}"); + break; + } + } + + private void SetAvailableOptions() + { + if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("keyboard")) + { + ShowAll(); + _leftStickController.Hide(); + _rightStickController.Hide(); + _deadZoneLeftBox.Hide(); + _deadZoneRightBox.Hide(); + _triggerThresholdBox.Hide(); + } + else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller")) + { + ShowAll(); + _leftStickKeyboard.Hide(); + _rightStickKeyboard.Hide(); + } + else + { + _settingsBox.Hide(); + } + + ClearValues(); + } + + private void SetCurrentValues() + { + SetControllerSpecificFields(); + + SetProfiles(); + + if (_inputDevice.ActiveId.StartsWith("keyboard") && _inputConfig is KeyboardConfig) + { + SetValues(_inputConfig); + } + else if (_inputDevice.ActiveId.StartsWith("controller") && _inputConfig is ControllerConfig) + { + SetValues(_inputConfig); + } + } + + private void SetControllerSpecificFields() + { + _leftSideTriggerBox.Hide(); + _rightSideTriggerBox.Hide(); + _altBox.Hide(); + + switch (_controllerType.ActiveId) + { + case "JoyconLeft": + _leftSideTriggerBox.Show(); + break; + case "JoyconRight": + _rightSideTriggerBox.Show(); + break; + case "JoyconPair": + _altBox.Show(); + break; + } + + _controllerImage.Pixbuf = _controllerType.ActiveId switch + { + "ProController" => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_ProCon.svg", 400, 400), + "JoyconLeft" => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_JoyConLeft.svg", 400, 400), + "JoyconRight" => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_JoyConRight.svg", 400, 400), + _ => new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Controller_JoyConPair.svg", 400, 400), + }; + } + + private void ClearValues() + { + _lStickX.Label = "Unbound"; + _lStickY.Label = "Unbound"; + _lStickUp.Label = "Unbound"; + _lStickDown.Label = "Unbound"; + _lStickLeft.Label = "Unbound"; + _lStickRight.Label = "Unbound"; + _lStickButton.Label = "Unbound"; + _dpadUp.Label = "Unbound"; + _dpadDown.Label = "Unbound"; + _dpadLeft.Label = "Unbound"; + _dpadRight.Label = "Unbound"; + _minus.Label = "Unbound"; + _l.Label = "Unbound"; + _zL.Label = "Unbound"; + _lSl.Label = "Unbound"; + _lSr.Label = "Unbound"; + _rStickX.Label = "Unbound"; + _rStickY.Label = "Unbound"; + _rStickUp.Label = "Unbound"; + _rStickDown.Label = "Unbound"; + _rStickLeft.Label = "Unbound"; + _rStickRight.Label = "Unbound"; + _rStickButton.Label = "Unbound"; + _a.Label = "Unbound"; + _b.Label = "Unbound"; + _x.Label = "Unbound"; + _y.Label = "Unbound"; + _plus.Label = "Unbound"; + _r.Label = "Unbound"; + _zR.Label = "Unbound"; + _rSl.Label = "Unbound"; + _rSr.Label = "Unbound"; + _controllerDeadzoneLeft.Value = 0; + _controllerDeadzoneRight.Value = 0; + _controllerTriggerThreshold.Value = 0; + _mirrorInput.Active = false; + _enableMotion.Active = false; + _slotNumber.Value = 0; + _altSlotNumber.Value = 0; + _sensitivity.Value = 100; + _gyroDeadzone.Value = 1; + _dsuServerHost.Buffer.Text = ""; + _dsuServerPort.Buffer.Text = ""; + } + + private void SetValues(InputConfig config) + { + switch (config) + { + case KeyboardConfig keyboardConfig: + if (!_controllerType.SetActiveId(keyboardConfig.ControllerType.ToString())) + { + _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld + ? ControllerType.Handheld.ToString() + : ControllerType.ProController.ToString()); + } + + _lStickUp.Label = keyboardConfig.LeftJoycon.StickUp.ToString(); + _lStickDown.Label = keyboardConfig.LeftJoycon.StickDown.ToString(); + _lStickLeft.Label = keyboardConfig.LeftJoycon.StickLeft.ToString(); + _lStickRight.Label = keyboardConfig.LeftJoycon.StickRight.ToString(); + _lStickButton.Label = keyboardConfig.LeftJoycon.StickButton.ToString(); + _dpadUp.Label = keyboardConfig.LeftJoycon.DPadUp.ToString(); + _dpadDown.Label = keyboardConfig.LeftJoycon.DPadDown.ToString(); + _dpadLeft.Label = keyboardConfig.LeftJoycon.DPadLeft.ToString(); + _dpadRight.Label = keyboardConfig.LeftJoycon.DPadRight.ToString(); + _minus.Label = keyboardConfig.LeftJoycon.ButtonMinus.ToString(); + _l.Label = keyboardConfig.LeftJoycon.ButtonL.ToString(); + _zL.Label = keyboardConfig.LeftJoycon.ButtonZl.ToString(); + _lSl.Label = keyboardConfig.LeftJoycon.ButtonSl.ToString(); + _lSr.Label = keyboardConfig.LeftJoycon.ButtonSr.ToString(); + _rStickUp.Label = keyboardConfig.RightJoycon.StickUp.ToString(); + _rStickDown.Label = keyboardConfig.RightJoycon.StickDown.ToString(); + _rStickLeft.Label = keyboardConfig.RightJoycon.StickLeft.ToString(); + _rStickRight.Label = keyboardConfig.RightJoycon.StickRight.ToString(); + _rStickButton.Label = keyboardConfig.RightJoycon.StickButton.ToString(); + _a.Label = keyboardConfig.RightJoycon.ButtonA.ToString(); + _b.Label = keyboardConfig.RightJoycon.ButtonB.ToString(); + _x.Label = keyboardConfig.RightJoycon.ButtonX.ToString(); + _y.Label = keyboardConfig.RightJoycon.ButtonY.ToString(); + _plus.Label = keyboardConfig.RightJoycon.ButtonPlus.ToString(); + _r.Label = keyboardConfig.RightJoycon.ButtonR.ToString(); + _zR.Label = keyboardConfig.RightJoycon.ButtonZr.ToString(); + _rSl.Label = keyboardConfig.RightJoycon.ButtonSl.ToString(); + _rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString(); + _slotNumber.Value = keyboardConfig.Slot; + _altSlotNumber.Value = keyboardConfig.AltSlot; + _sensitivity.Value = keyboardConfig.Sensitivity; + _gyroDeadzone.Value = keyboardConfig.GyroDeadzone; + _enableMotion.Active = keyboardConfig.EnableMotion; + _mirrorInput.Active = keyboardConfig.MirrorInput; + _dsuServerHost.Buffer.Text = keyboardConfig.DsuServerHost; + _dsuServerPort.Buffer.Text = keyboardConfig.DsuServerPort.ToString(); + break; + case ControllerConfig controllerConfig: + if (!_controllerType.SetActiveId(controllerConfig.ControllerType.ToString())) + { + _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld + ? ControllerType.Handheld.ToString() + : ControllerType.ProController.ToString()); + } + + _lStickX.Label = controllerConfig.LeftJoycon.StickX.ToString(); + _invertLStickX.Active = controllerConfig.LeftJoycon.InvertStickX; + _lStickY.Label = controllerConfig.LeftJoycon.StickY.ToString(); + _invertLStickY.Active = controllerConfig.LeftJoycon.InvertStickY; + _lStickButton.Label = controllerConfig.LeftJoycon.StickButton.ToString(); + _dpadUp.Label = controllerConfig.LeftJoycon.DPadUp.ToString(); + _dpadDown.Label = controllerConfig.LeftJoycon.DPadDown.ToString(); + _dpadLeft.Label = controllerConfig.LeftJoycon.DPadLeft.ToString(); + _dpadRight.Label = controllerConfig.LeftJoycon.DPadRight.ToString(); + _minus.Label = controllerConfig.LeftJoycon.ButtonMinus.ToString(); + _l.Label = controllerConfig.LeftJoycon.ButtonL.ToString(); + _zL.Label = controllerConfig.LeftJoycon.ButtonZl.ToString(); + _lSl.Label = controllerConfig.LeftJoycon.ButtonSl.ToString(); + _lSr.Label = controllerConfig.LeftJoycon.ButtonSr.ToString(); + _rStickX.Label = controllerConfig.RightJoycon.StickX.ToString(); + _invertRStickX.Active = controllerConfig.RightJoycon.InvertStickX; + _rStickY.Label = controllerConfig.RightJoycon.StickY.ToString(); + _invertRStickY.Active = controllerConfig.RightJoycon.InvertStickY; + _rStickButton.Label = controllerConfig.RightJoycon.StickButton.ToString(); + _a.Label = controllerConfig.RightJoycon.ButtonA.ToString(); + _b.Label = controllerConfig.RightJoycon.ButtonB.ToString(); + _x.Label = controllerConfig.RightJoycon.ButtonX.ToString(); + _y.Label = controllerConfig.RightJoycon.ButtonY.ToString(); + _plus.Label = controllerConfig.RightJoycon.ButtonPlus.ToString(); + _r.Label = controllerConfig.RightJoycon.ButtonR.ToString(); + _zR.Label = controllerConfig.RightJoycon.ButtonZr.ToString(); + _rSl.Label = controllerConfig.RightJoycon.ButtonSl.ToString(); + _rSr.Label = controllerConfig.RightJoycon.ButtonSr.ToString(); + _controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft; + _controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight; + _controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold; + _slotNumber.Value = controllerConfig.Slot; + _altSlotNumber.Value = controllerConfig.AltSlot; + _sensitivity.Value = controllerConfig.Sensitivity; + _gyroDeadzone.Value = controllerConfig.GyroDeadzone; + _enableMotion.Active = controllerConfig.EnableMotion; + _mirrorInput.Active = controllerConfig.MirrorInput; + _dsuServerHost.Buffer.Text = controllerConfig.DsuServerHost; + _dsuServerPort.Buffer.Text = controllerConfig.DsuServerPort.ToString(); + break; + } + } + + private InputConfig GetValues() + { + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + Enum.TryParse(_lStickUp.Label, out Key lStickUp); + Enum.TryParse(_lStickDown.Label, out Key lStickDown); + Enum.TryParse(_lStickLeft.Label, out Key lStickLeft); + Enum.TryParse(_lStickRight.Label, out Key lStickRight); + Enum.TryParse(_lStickButton.Label, out Key lStickButton); + Enum.TryParse(_dpadUp.Label, out Key lDPadUp); + Enum.TryParse(_dpadDown.Label, out Key lDPadDown); + Enum.TryParse(_dpadLeft.Label, out Key lDPadLeft); + Enum.TryParse(_dpadRight.Label, out Key lDPadRight); + Enum.TryParse(_minus.Label, out Key lButtonMinus); + Enum.TryParse(_l.Label, out Key lButtonL); + Enum.TryParse(_zL.Label, out Key lButtonZl); + Enum.TryParse(_lSl.Label, out Key lButtonSl); + Enum.TryParse(_lSr.Label, out Key lButtonSr); + + Enum.TryParse(_rStickUp.Label, out Key rStickUp); + Enum.TryParse(_rStickDown.Label, out Key rStickDown); + Enum.TryParse(_rStickLeft.Label, out Key rStickLeft); + Enum.TryParse(_rStickRight.Label, out Key rStickRight); + Enum.TryParse(_rStickButton.Label, out Key rStickButton); + Enum.TryParse(_a.Label, out Key rButtonA); + Enum.TryParse(_b.Label, out Key rButtonB); + Enum.TryParse(_x.Label, out Key rButtonX); + Enum.TryParse(_y.Label, out Key rButtonY); + Enum.TryParse(_plus.Label, out Key rButtonPlus); + Enum.TryParse(_r.Label, out Key rButtonR); + Enum.TryParse(_zR.Label, out Key rButtonZr); + Enum.TryParse(_rSl.Label, out Key rButtonSl); + Enum.TryParse(_rSr.Label, out Key rButtonSr); + + int.TryParse(_dsuServerPort.Buffer.Text, out int port); + + return new KeyboardConfig + { + Index = int.Parse(_inputDevice.ActiveId.Split("/")[1]), + ControllerType = Enum.Parse(_controllerType.ActiveId), + PlayerIndex = _playerIndex, + LeftJoycon = new NpadKeyboardLeft + { + StickUp = lStickUp, + StickDown = lStickDown, + StickLeft = lStickLeft, + StickRight = lStickRight, + StickButton = lStickButton, + DPadUp = lDPadUp, + DPadDown = lDPadDown, + DPadLeft = lDPadLeft, + DPadRight = lDPadRight, + ButtonMinus = lButtonMinus, + ButtonL = lButtonL, + ButtonZl = lButtonZl, + ButtonSl = lButtonSl, + ButtonSr = lButtonSr + }, + RightJoycon = new NpadKeyboardRight + { + StickUp = rStickUp, + StickDown = rStickDown, + StickLeft = rStickLeft, + StickRight = rStickRight, + StickButton = rStickButton, + ButtonA = rButtonA, + ButtonB = rButtonB, + ButtonX = rButtonX, + ButtonY = rButtonY, + ButtonPlus = rButtonPlus, + ButtonR = rButtonR, + ButtonZr = rButtonZr, + ButtonSl = rButtonSl, + ButtonSr = rButtonSr + }, + EnableMotion = _enableMotion.Active, + MirrorInput = _mirrorInput.Active, + Slot = (int)_slotNumber.Value, + AltSlot = (int)_altSlotNumber.Value, + Sensitivity = (int)_sensitivity.Value, + GyroDeadzone = _gyroDeadzone.Value, + DsuServerHost = _dsuServerHost.Buffer.Text, + DsuServerPort = port + }; + } + + if (_inputDevice.ActiveId.StartsWith("controller")) + { + Enum.TryParse(_lStickX.Label, out ControllerInputId lStickX); + Enum.TryParse(_lStickY.Label, out ControllerInputId lStickY); + Enum.TryParse(_lStickButton.Label, out ControllerInputId lStickButton); + Enum.TryParse(_minus.Label, out ControllerInputId lButtonMinus); + Enum.TryParse(_l.Label, out ControllerInputId lButtonL); + Enum.TryParse(_zL.Label, out ControllerInputId lButtonZl); + Enum.TryParse(_lSl.Label, out ControllerInputId lButtonSl); + Enum.TryParse(_lSr.Label, out ControllerInputId lButtonSr); + Enum.TryParse(_dpadUp.Label, out ControllerInputId lDPadUp); + Enum.TryParse(_dpadDown.Label, out ControllerInputId lDPadDown); + Enum.TryParse(_dpadLeft.Label, out ControllerInputId lDPadLeft); + Enum.TryParse(_dpadRight.Label, out ControllerInputId lDPadRight); + + Enum.TryParse(_rStickX.Label, out ControllerInputId rStickX); + Enum.TryParse(_rStickY.Label, out ControllerInputId rStickY); + Enum.TryParse(_rStickButton.Label, out ControllerInputId rStickButton); + Enum.TryParse(_a.Label, out ControllerInputId rButtonA); + Enum.TryParse(_b.Label, out ControllerInputId rButtonB); + Enum.TryParse(_x.Label, out ControllerInputId rButtonX); + Enum.TryParse(_y.Label, out ControllerInputId rButtonY); + Enum.TryParse(_plus.Label, out ControllerInputId rButtonPlus); + Enum.TryParse(_r.Label, out ControllerInputId rButtonR); + Enum.TryParse(_zR.Label, out ControllerInputId rButtonZr); + Enum.TryParse(_rSl.Label, out ControllerInputId rButtonSl); + Enum.TryParse(_rSr.Label, out ControllerInputId rButtonSr); + + int.TryParse(_dsuServerPort.Buffer.Text, out int port); + + return new ControllerConfig + { + Index = int.Parse(_inputDevice.ActiveId.Split("/")[1]), + ControllerType = Enum.Parse(_controllerType.ActiveId), + PlayerIndex = _playerIndex, + DeadzoneLeft = (float)_controllerDeadzoneLeft.Value, + DeadzoneRight = (float)_controllerDeadzoneRight.Value, + TriggerThreshold = (float)_controllerTriggerThreshold.Value, + LeftJoycon = new NpadControllerLeft + { + InvertStickX = _invertLStickX.Active, + StickX = lStickX, + InvertStickY = _invertLStickY.Active, + StickY = lStickY, + StickButton = lStickButton, + ButtonMinus = lButtonMinus, + ButtonL = lButtonL, + ButtonZl = lButtonZl, + ButtonSl = lButtonSl, + ButtonSr = lButtonSr, + DPadUp = lDPadUp, + DPadDown = lDPadDown, + DPadLeft = lDPadLeft, + DPadRight = lDPadRight + }, + RightJoycon = new NpadControllerRight + { + InvertStickX = _invertRStickX.Active, + StickX = rStickX, + InvertStickY = _invertRStickY.Active, + StickY = rStickY, + StickButton = rStickButton, + ButtonA = rButtonA, + ButtonB = rButtonB, + ButtonX = rButtonX, + ButtonY = rButtonY, + ButtonPlus = rButtonPlus, + ButtonR = rButtonR, + ButtonZr = rButtonZr, + ButtonSl = rButtonSl, + ButtonSr = rButtonSr + }, + EnableMotion = _enableMotion.Active, + MirrorInput = _mirrorInput.Active, + Slot = (int)_slotNumber.Value, + AltSlot = (int)_altSlotNumber.Value, + Sensitivity = (int)_sensitivity.Value, + GyroDeadzone = _gyroDeadzone.Value, + DsuServerHost = _dsuServerHost.Buffer.Text, + DsuServerPort = port + }; + } + + if (!_inputDevice.ActiveId.StartsWith("disabled")) + { + GtkDialog.CreateErrorDialog("Some fields entered where invalid and therefore your config was not saved."); + } + + return null; + } + + private static bool IsAnyKeyPressed(out Key pressedKey, int index) + { + KeyboardState keyboardState = KeyboardController.GetKeyboardState(index); + + foreach (Key key in Enum.GetValues(typeof(Key))) + { + if (keyboardState.IsKeyDown((OpenTK.Input.Key)key)) + { + pressedKey = key; + + return true; + } + } + + pressedKey = Key.Unbound; + + return false; + } + + private static bool IsAnyButtonPressed(out ControllerInputId pressedButton, int index, double triggerThreshold) + { + JoystickState joystickState = Joystick.GetState(index); + JoystickCapabilities joystickCapabilities = Joystick.GetCapabilities(index); + + //Buttons + for (int i = 0; i != joystickCapabilities.ButtonCount; i++) + { + if (joystickState.IsButtonDown(i)) + { + Enum.TryParse($"Button{i}", out pressedButton); + return true; + } + } + + //Axis + for (int i = 0; i != joystickCapabilities.AxisCount; i++) + { + if (joystickState.GetAxis(i) > 0.5f && joystickState.GetAxis(i) > triggerThreshold) + { + Enum.TryParse($"Axis{i}", out pressedButton); + + return true; + } + } + + //Hats + for (int i = 0; i != joystickCapabilities.HatCount; i++) + { + JoystickHatState hatState = joystickState.GetHat((JoystickHat)i); + string pos = null; + + if (hatState.IsUp) pos = "Up"; + if (hatState.IsDown) pos = "Down"; + if (hatState.IsLeft) pos = "Left"; + if (hatState.IsRight) pos = "Right"; + if (pos == null) continue; + + Enum.TryParse($"Hat{i}{pos}", out pressedButton); + + return true; + } + + pressedButton = ControllerInputId.Unbound; + + return false; + } + + private string GetProfileBasePath() + { + string path = AppDataManager.ProfilesDirPath; + + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + path = System.IO.Path.Combine(path, "keyboard"); + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + path = System.IO.Path.Combine(path, "controller"); + } + + return path; + } + + // + // Events + // + private void InputDevice_Changed(object sender, EventArgs args) + { + SetAvailableOptions(); + SetControllerSpecificFields(); + + if (_inputDevice.ActiveId != null) SetProfiles(); + } + + private void Controller_Changed(object sender, EventArgs args) + { + SetControllerSpecificFields(); + } + + private void RefreshInputDevicesButton_Pressed(object sender, EventArgs args) + { + UpdateInputDeviceList(); + + _refreshInputDevicesButton.SetStateFlags(StateFlags.Normal, true); + } + + private void Button_Pressed(object sender, EventArgs args) + { + if (_isWaitingForInput) + { + return; + } + + _isWaitingForInput = true; + + Thread inputThread = new Thread(() => + { + Button button = (ToggleButton)sender; + + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + Key pressedKey; + + int index = int.Parse(_inputDevice.ActiveId.Split("/")[1]); + while (!IsAnyKeyPressed(out pressedKey, index)) + { + if (Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsKeyDown(OpenTK.Input.Key.Escape)) + { + Application.Invoke(delegate + { + button.SetStateFlags(StateFlags.Normal, true); + }); + + _isWaitingForInput = false; + + return; + } + } + + Application.Invoke(delegate + { + button.Label = pressedKey.ToString(); + button.SetStateFlags(StateFlags.Normal, true); + }); + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + ControllerInputId pressedButton; + + int index = int.Parse(_inputDevice.ActiveId.Split("/")[1]); + while (!IsAnyButtonPressed(out pressedButton, index, _controllerTriggerThreshold.Value)) + { + if (Mouse.GetState().IsAnyButtonDown || Keyboard.GetState().IsAnyKeyDown) + { + Application.Invoke(delegate + { + button.SetStateFlags(StateFlags.Normal, true); + }); + + _isWaitingForInput = false; + + return; + } + } + + Application.Invoke(delegate + { + button.Label = pressedButton.ToString(); + button.SetStateFlags(StateFlags.Normal, true); + }); + } + + _isWaitingForInput = false; + }); + inputThread.Name = "GUI.InputThread"; + inputThread.IsBackground = true; + inputThread.Start(); + } + + private void SetProfiles() + { + string basePath = GetProfileBasePath(); + + if (!Directory.Exists(basePath)) + { + Directory.CreateDirectory(basePath); + } + + _profile.RemoveAll(); + _profile.Append("default", "Default"); + + foreach (string profile in Directory.GetFiles(basePath, "*.*", SearchOption.AllDirectories)) + { + _profile.Append(System.IO.Path.GetFileName(profile), System.IO.Path.GetFileNameWithoutExtension(profile)); + } + } + + private void ProfileLoad_Activated(object sender, EventArgs args) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == null) return; + + InputConfig config = null; + int pos = _profile.Active; + + if (_profile.ActiveId == "default") + { + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + config = new KeyboardConfig + { + Index = 0, + ControllerType = ControllerType.JoyconPair, + LeftJoycon = new NpadKeyboardLeft + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F, + DPadUp = Key.Up, + DPadDown = Key.Down, + DPadLeft = Key.Left, + DPadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound + }, + RightJoycon = new NpadKeyboardRight + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H, + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound + }, + EnableMotion = false, + MirrorInput = false, + Slot = 0, + AltSlot = 0, + Sensitivity = 100, + GyroDeadzone = 1, + DsuServerHost = "127.0.0.1", + DsuServerPort = 26760 + }; + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + config = new ControllerConfig + { + Index = 0, + ControllerType = ControllerType.ProController, + DeadzoneLeft = 0.1f, + DeadzoneRight = 0.1f, + TriggerThreshold = 0.5f, + LeftJoycon = new NpadControllerLeft + { + StickX = ControllerInputId.Axis0, + StickY = ControllerInputId.Axis1, + StickButton = ControllerInputId.Button8, + DPadUp = ControllerInputId.Hat0Up, + DPadDown = ControllerInputId.Hat0Down, + DPadLeft = ControllerInputId.Hat0Left, + DPadRight = ControllerInputId.Hat0Right, + ButtonMinus = ControllerInputId.Button6, + ButtonL = ControllerInputId.Button4, + ButtonZl = ControllerInputId.Axis2, + ButtonSl = ControllerInputId.Unbound, + ButtonSr = ControllerInputId.Unbound, + InvertStickX = false, + InvertStickY = false + }, + RightJoycon = new NpadControllerRight + { + StickX = ControllerInputId.Axis3, + StickY = ControllerInputId.Axis4, + StickButton = ControllerInputId.Button9, + ButtonA = ControllerInputId.Button1, + ButtonB = ControllerInputId.Button0, + ButtonX = ControllerInputId.Button3, + ButtonY = ControllerInputId.Button2, + ButtonPlus = ControllerInputId.Button7, + ButtonR = ControllerInputId.Button5, + ButtonZr = ControllerInputId.Axis5, + ButtonSl = ControllerInputId.Unbound, + ButtonSr = ControllerInputId.Unbound, + InvertStickX = false, + InvertStickY = false + }, + EnableMotion = false, + MirrorInput = false, + Slot = 0, + AltSlot = 0, + Sensitivity = 100, + GyroDeadzone = 1, + DsuServerHost = "127.0.0.1", + DsuServerPort = 26760 + }; + } + } + else + { + string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId); + + if (!File.Exists(path)) + { + if (pos >= 0) + { + _profile.Remove(pos); + } + + return; + } + + try + { + using (Stream stream = File.OpenRead(path)) + { + config = JsonHelper.Deserialize(stream); + } + } + catch (JsonException) + { + try + { + using (Stream stream = File.OpenRead(path)) + { + config = JsonHelper.Deserialize(stream); + } + } + catch { } + } + } + + SetValues(config); + } + + private void ProfileAdd_Activated(object sender, EventArgs args) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + if (_inputDevice.ActiveId == "disabled") return; + + InputConfig inputConfig = GetValues(); + ProfileDialog profileDialog = new ProfileDialog(); + + if (inputConfig == null) return; + + if (profileDialog.Run() == (int)ResponseType.Ok) + { + string path = System.IO.Path.Combine(GetProfileBasePath(), profileDialog.FileName); + string jsonString; + + if (inputConfig is KeyboardConfig keyboardConfig) + { + jsonString = JsonHelper.Serialize(keyboardConfig, true); + } + else + { + jsonString = JsonHelper.Serialize(inputConfig as ControllerConfig, true); + } + + File.WriteAllText(path, jsonString); + } + + profileDialog.Dispose(); + + SetProfiles(); + } + + private void ProfileRemove_Activated(object sender, EventArgs args) + { + ((ToggleButton) sender).SetStateFlags(StateFlags.Normal, true); + + if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == "default" || _profile.ActiveId == null) return; + + MessageDialog confirmDialog = GtkDialog.CreateConfirmationDialog("Deleting Profile", "This action is irreversible, are your sure you want to continue?"); + + if (confirmDialog.Run() == (int)ResponseType.Yes) + { + string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId); + + if (File.Exists(path)) + { + File.Delete(path); + } + + SetProfiles(); + } + } + + private void SaveToggle_Activated(object sender, EventArgs args) + { + InputConfig inputConfig = GetValues(); + + var newConfig = new List(); + newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); + + if (_inputConfig == null && inputConfig != null) + { + newConfig.Add(inputConfig); + } + else + { + if (_inputDevice.ActiveId == "disabled") + { + newConfig.Remove(_inputConfig); + } + else if (inputConfig != null) + { + int index = newConfig.IndexOf(_inputConfig); + + newConfig[index] = inputConfig; + } + } + + // Atomically replace and signal input change. + // NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event. + ConfigurationState.Instance.Hid.InputConfig.Value = newConfig; + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + Dispose(); + } + + private void CloseToggle_Activated(object sender, EventArgs args) + { + Dispose(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Windows/ControllerWindow.glade b/Ryujinx/Ui/Windows/ControllerWindow.glade new file mode 100644 index 00000000..2143e9de --- /dev/null +++ b/Ryujinx/Ui/Windows/ControllerWindow.glade @@ -0,0 +1,2038 @@ + + + + + + 4 + 1 + 4 + + + 1 + 0.050000000000000003 + 0.01 + 0.10000000000000001 + + + 1 + 0.050000000000000003 + 0.01 + 0.10000000000000001 + + + 1 + 0.5 + 0.01 + 0.10000000000000001 + + + 100 + 0.01 + 0.01 + 0.10000000000000001 + 0.10000000000000001 + + + 1000 + 100 + 1 + 4 + + + 4 + 1 + 4 + + + False + Ryujinx - Controller Settings + True + center + 1100 + 600 + + + + + + True + False + vertical + + + True + True + in + + + True + False + + + True + False + vertical + + + True + False + 10 + 10 + 10 + + + True + False + + + True + False + 5 + Input Device + + + False + True + 0 + + + + + True + False + 0 + disabled + + Disabled + + + + + True + True + 1 + + + + + Refresh + True + True + True + 5 + + + + False + True + 2 + + + + + False + True + 0 + + + + + True + False + 20 + + + True + False + The controller's type + center + 5 + Controller Type: + + + False + True + 0 + + + + + True + False + The controller's type + 0 + + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 20 + + + True + False + 5 + Profile: + + + False + True + 0 + + + + + True + False + 5 + 0 + default + + + False + True + 1 + + + + + Load + 60 + True + True + True + 5 + + + + False + True + 2 + + + + + Add + 60 + True + True + True + 5 + + + + False + True + 3 + + + + + Remove + 60 + True + True + True + + + + False + True + 4 + + + + + False + True + 2 + + + + + False + True + 0 + + + + + True + False + + + True + False + vertical + + + True + False + 10 + 5 + + + 156 + True + False + 10 + vertical + + + True + False + 5 + 5 + Buttons + + + + + + False + True + 0 + + + + + True + False + 3 + 10 + + + 80 + True + False + A + + + 0 + 0 + + + + + 80 + True + False + B + + + 0 + 1 + + + + + 80 + True + False + X + + + 0 + 2 + + + + + 80 + True + False + Y + + + 0 + 3 + + + + + + 70 + True + True + True + + + 1 + 0 + + + + + + 70 + True + True + True + + + 1 + 1 + + + + + + 70 + True + True + True + + + 1 + 2 + + + + + + 70 + True + True + True + + + 1 + 3 + + + + + 80 + True + False + + + + + 0 + 4 + + + + + 80 + True + False + - + + + 0 + 5 + + + + + + 70 + True + True + True + + + 1 + 5 + + + + + + 70 + True + True + True + + + 1 + 4 + + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + + + False + True + 1 + + + + + 160 + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + Left Stick + + + + + + False + True + 0 + + + + + True + False + 5 + 3 + 10 + + + 80 + True + False + LStick Button + 0 + + + 0 + 0 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + False + True + 1 + + + + + True + False + 3 + 10 + + + + 65 + True + True + True + + + 1 + 1 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 2 + + + + + + 65 + True + True + True + + + 1 + 3 + + + + + 80 + True + False + LStick Down + 0 + + + 0 + 1 + + + + + 80 + True + False + LStick Up + 0 + + + 0 + 0 + + + + + 80 + True + False + LStick Right + 0 + + + 0 + 3 + + + + + 80 + True + False + LStick Left + 0 + + + 0 + 2 + + + + + False + True + 2 + + + + + True + False + 3 + 10 + + + 80 + True + False + LStick Lt/Rt + 0 + + + 0 + 0 + + + + + 80 + True + False + LStick Up/Dn + 0 + + + 0 + 1 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 1 + + + + + Invert + True + True + False + True + + + 2 + 0 + + + + + Invert + True + True + False + True + + + 2 + 1 + + + + + False + True + 3 + + + + + True + False + 10 + vertical + + + True + False + start + Deadzone Left + + + False + True + 0 + + + + + True + True + _controllerDeadzoneLeft + 2 + 2 + + + True + True + 1 + + + + + False + True + 4 + + + + + False + True + 2 + + + + + True + False + + + False + True + 3 + + + + + 150 + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + Triggers + + + + + + False + True + 0 + + + + + True + False + 3 + 10 + + + 80 + True + False + L + + + 0 + 0 + + + + + 80 + True + False + R + + + 0 + 1 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 1 + + + + + 80 + True + False + ZL + + + 0 + 2 + + + + + 80 + True + False + ZR + + + 0 + 3 + + + + + + 65 + True + True + True + + + 1 + 2 + + + + + + 65 + True + True + True + + + 1 + 3 + + + + + False + True + 1 + + + + + _sideTriggerBox + True + False + 5 + 3 + 10 + + + 80 + True + False + Left SL + + + 0 + 0 + + + + + 80 + True + False + Left SR + + + 0 + 1 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 1 + + + + + False + True + 2 + + + + + _sideTriggerBox + True + False + 5 + 3 + 10 + + + 80 + True + False + Right SL + + + 0 + 0 + + + + + 80 + True + False + Right SR + + + 0 + 1 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 1 + + + + + False + True + 3 + + + + + True + False + 10 + vertical + + + True + False + start + 10 + Trigger Threshold + + + False + True + 0 + + + + + True + True + _controllerTriggerThreshold + 2 + 2 + + + True + True + 1 + + + + + False + True + 4 + + + + + False + True + 4 + + + + + False + True + 0 + + + + + True + False + 10 + + + 156 + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + Directional Pad + + + + + + False + True + 0 + + + + + True + False + 3 + 10 + + + 80 + True + False + Dpad Up + 0 + + + 0 + 0 + + + + + 80 + True + False + Dpad Down + 0 + + + 0 + 1 + + + + + 80 + True + False + Dpad Left + 0 + + + 0 + 2 + + + + + 80 + True + False + Dpad Right + 0 + + + 0 + 3 + + + + + + 70 + True + True + True + + + 1 + 0 + + + + + + 70 + True + True + True + + + 1 + 1 + + + + + + 70 + True + True + True + + + 1 + 2 + + + + + + 70 + True + True + True + + + 1 + 3 + + + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + + + False + True + 1 + + + + + 160 + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + Right Stick + + + + + + False + True + 0 + + + + + True + False + 5 + 3 + 10 + + + 80 + True + False + RStick Button + 0 + + + 0 + 0 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + False + True + 1 + + + + + True + False + 3 + 10 + + + 80 + True + False + RStick Up + 0 + + + 0 + 0 + + + + + 80 + True + False + RStick Down + 0 + + + 0 + 1 + + + + + 80 + True + False + RStick Left + 0 + + + 0 + 2 + + + + + 80 + True + False + RStick Right + 0 + + + 0 + 3 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 1 + + + + + + 65 + True + True + True + + + 1 + 2 + + + + + + 65 + True + True + True + + + 1 + 3 + + + + + False + True + 2 + + + + + True + False + 3 + 10 + + + 80 + True + False + RStick Lt/Rt + 0 + + + 0 + 0 + + + + + 80 + True + False + RStick Up/Dn + 0 + + + 0 + 1 + + + + + + 65 + True + True + True + + + 1 + 0 + + + + + + 65 + True + True + True + + + 1 + 1 + + + + + Invert + True + True + False + True + + + 2 + 0 + + + + + Invert + True + True + False + True + + + 2 + 1 + + + + + False + True + 3 + + + + + True + False + 10 + vertical + + + True + False + start + Deadzone Right + + + False + True + 0 + + + + + True + True + _controllerDeadzoneRight + 2 + 2 + + + True + True + 1 + + + + + False + True + 4 + + + + + False + True + 2 + + + + + True + False + + + False + True + 3 + + + + + True + False + 10 + 10 + vertical + 5 + + + True + False + 5 + 5 + Motion + + + + + + False + True + 0 + + + + + Enable Motion Controls + True + True + False + True + + + False + True + 1 + + + + + True + False + 10 + + + True + False + 17 + Controller Slot + + + False + True + 5 + 0 + + + + + True + True + 10 + _slotNumber + 1 + True + True + + + False + True + 1 + + + + + False + True + 5 + 2 + + + + + True + False + 10 + + + True + False + 5 + Gyro Sensitivity % + + + False + True + 5 + 0 + + + + + True + True + 0 + _sensitivity + 1 + True + True + + + False + True + 1 + + + + + False + True + 5 + 3 + + + + + True + False + vertical + + + Mirror Input + True + True + False + True + + + False + True + 0 + + + + + True + False + 10 + + + True + False + Right JoyCon Slot + + + False + True + 5 + 0 + + + + + True + True + 0 + _altSlotNumber + 1 + True + True + + + False + True + 1 + + + + + False + True + 5 + 1 + + + + + False + True + 4 + + + + + True + False + 30 + + + True + False + Server Host + + + False + True + 5 + 0 + + + + + True + True + + + False + True + 1 + + + + + False + True + 5 + 5 + + + + + True + False + 30 + + + True + False + Server Port + + + False + True + 5 + 0 + + + + + True + True + + + False + True + 1 + + + + + False + True + 5 + 6 + + + + + True + False + start + Gyro Deadzone + + + False + True + 7 + + + + + True + True + _gyroDeadzone + 2 + 2 + + + True + True + 8 + + + + + False + True + 4 + + + + + False + True + 1 + + + + + True + True + 0 + + + + + True + False + 10 + 20 + 5 + 5 + + + True + True + 1 + + + + + True + True + 1 + + + + + + + + + True + True + 0 + + + + + True + False + 5 + 3 + 3 + end + + + Save + True + True + True + + + + False + True + 0 + + + + + Close + True + True + True + 4 + + + + False + True + 5 + 1 + + + + + False + False + 1 + + + + + + diff --git a/Ryujinx/Ui/Windows/DlcWindow.cs b/Ryujinx/Ui/Windows/DlcWindow.cs new file mode 100644 index 00000000..13a63088 --- /dev/null +++ b/Ryujinx/Ui/Windows/DlcWindow.cs @@ -0,0 +1,253 @@ +using Gtk; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; +using Ryujinx.Common.Configuration; +using Ryujinx.HLE.FileSystem; +using Ryujinx.Ui.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +using GUI = Gtk.Builder.ObjectAttribute; +using JsonHelper = Ryujinx.Common.Utilities.JsonHelper; + +namespace Ryujinx.Ui.Windows +{ + public class DlcWindow : Window + { + private readonly VirtualFileSystem _virtualFileSystem; + private readonly string _titleId; + private readonly string _dlcJsonPath; + private readonly List _dlcContainerList; + +#pragma warning disable CS0649, IDE0044 + [GUI] Label _baseTitleInfoLabel; + [GUI] TreeView _dlcTreeView; + [GUI] TreeSelection _dlcTreeSelection; +#pragma warning restore CS0649, IDE0044 + + public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { } + + private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetObject("_dlcWindow").Handle) + { + builder.Autoconnect(this); + + _titleId = titleId; + _virtualFileSystem = virtualFileSystem; + _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json"); + _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]"; + + try + { + _dlcContainerList = JsonHelper.DeserializeFromFile>(_dlcJsonPath); + } + catch + { + _dlcContainerList = new List(); + } + + _dlcTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string)); + + CellRendererToggle enableToggle = new CellRendererToggle(); + enableToggle.Toggled += (sender, args) => + { + _dlcTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path)); + bool newValue = !(bool)_dlcTreeView.Model.GetValue(treeIter, 0); + _dlcTreeView.Model.SetValue(treeIter, 0, newValue); + + if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, treeIter)) + { + do + { + _dlcTreeView.Model.SetValue(childIter, 0, newValue); + } + while (_dlcTreeView.Model.IterNext(ref childIter)); + } + }; + + _dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0); + _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1); + _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); + + foreach (DlcContainer dlcContainer in _dlcContainerList) + { + TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", dlcContainer.Path); + + using FileStream containerFile = File.OpenRead(dlcContainer.Path); + PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage()); + _virtualFileSystem.ImportTickets(pfs); + + foreach (DlcNca dlcNca in dlcContainer.DlcNcaList) + { + pfs.OpenFile(out IFile ncaFile, dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + Nca nca = TryCreateNca(ncaFile.AsStorage(), dlcContainer.Path); + + if (nca != null) + { + ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.Path); + } + } + } + } + + private Nca TryCreateNca(IStorage ncaStorage, string containerPath) + { + try + { + return new Nca(_virtualFileSystem.KeySet, ncaStorage); + } + catch (Exception exception) + { + GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {containerPath}"); + } + + return null; + } + + private void AddButton_Clicked(object sender, EventArgs args) + { + FileChooserDialog fileChooser = new FileChooserDialog("Select DLC files", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept) + { + SelectMultiple = true, + Filter = new FileFilter() + }; + fileChooser.SetPosition(WindowPosition.Center); + fileChooser.Filter.AddPattern("*.nsp"); + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + foreach (string containerPath in fileChooser.Filenames) + { + if (!File.Exists(containerPath)) + { + return; + } + + using (FileStream containerFile = File.OpenRead(containerPath)) + { + PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage()); + bool containsDlc = false; + + _virtualFileSystem.ImportTickets(pfs); + + TreeIter? parentIter = null; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryCreateNca(ncaFile.AsStorage(), containerPath); + + if (nca == null) continue; + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId) + { + break; + } + + parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath); + + ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); + containsDlc = true; + } + } + + if (!containsDlc) + { + GtkDialog.CreateErrorDialog("The specified file does not contain a DLC for the selected title!"); + } + } + } + } + + fileChooser.Dispose(); + } + + private void RemoveButton_Clicked(object sender, EventArgs args) + { + if (_dlcTreeSelection.GetSelected(out ITreeModel treeModel, out TreeIter treeIter)) + { + if (_dlcTreeView.Model.IterParent(out TreeIter parentIter, treeIter) && _dlcTreeView.Model.IterNChildren(parentIter) <= 1) + { + ((TreeStore)treeModel).Remove(ref parentIter); + } + else + { + ((TreeStore)treeModel).Remove(ref treeIter); + } + } + } + + private void RemoveAllButton_Clicked(object sender, EventArgs args) + { + List toRemove = new List(); + + if (_dlcTreeView.Model.GetIterFirst(out TreeIter iter)) + { + do + { + toRemove.Add(iter); + } + while (_dlcTreeView.Model.IterNext(ref iter)); + } + + foreach (TreeIter i in toRemove) + { + TreeIter j = i; + ((TreeStore)_dlcTreeView.Model).Remove(ref j); + } + } + + private void SaveButton_Clicked(object sender, EventArgs args) + { + _dlcContainerList.Clear(); + + if (_dlcTreeView.Model.GetIterFirst(out TreeIter parentIter)) + { + do + { + if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter)) + { + DlcContainer dlcContainer = new DlcContainer + { + Path = (string)_dlcTreeView.Model.GetValue(parentIter, 2), + DlcNcaList = new List() + }; + + do + { + dlcContainer.DlcNcaList.Add(new DlcNca + { + Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0), + TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16), + Path = (string)_dlcTreeView.Model.GetValue(childIter, 2) + }); + } + while (_dlcTreeView.Model.IterNext(ref childIter)); + + _dlcContainerList.Add(dlcContainer); + } + } + while (_dlcTreeView.Model.IterNext(ref parentIter)); + } + + using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough)) + { + dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true))); + } + + Dispose(); + } + + private void CancelButton_Clicked(object sender, EventArgs args) + { + Dispose(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Windows/DlcWindow.glade b/Ryujinx/Ui/Windows/DlcWindow.glade new file mode 100644 index 00000000..cd0d8674 --- /dev/null +++ b/Ryujinx/Ui/Windows/DlcWindow.glade @@ -0,0 +1,202 @@ + + + + + + False + Ryujinx - DLC Manager + True + center + 550 + 350 + + + True + False + vertical + + + True + False + vertical + + + True + False + 10 + 10 + 10 + 10 + Available DLC + + + False + True + 0 + + + + + True + True + 10 + 10 + in + + + True + False + + + True + True + False + + + + + + + + + + True + True + 1 + + + + + True + True + 0 + + + + + True + False + + + True + False + 10 + 10 + start + + + Add + True + True + True + Adds an update to this list + 10 + + + + True + True + 0 + + + + + Remove + True + True + True + Removes the selected update + 10 + + + + True + True + 1 + + + + + Remove All + True + True + True + Removes the selected update + 10 + + + + True + True + 2 + + + + + True + True + 0 + + + + + True + False + 10 + 10 + end + + + Save + True + True + True + 10 + 2 + 2 + + + + True + True + 0 + + + + + Cancel + True + True + True + 10 + 2 + 2 + + + + True + True + 1 + + + + + True + True + 1 + + + + + False + True + 1 + + + + + + + + + diff --git a/Ryujinx/Ui/Windows/SettingsWindow.cs b/Ryujinx/Ui/Windows/SettingsWindow.cs new file mode 100644 index 00000000..a44abfcc --- /dev/null +++ b/Ryujinx/Ui/Windows/SettingsWindow.cs @@ -0,0 +1,575 @@ +using Gtk; +using Ryujinx.Audio; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Configuration; +using Ryujinx.Configuration.System; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; +using Ryujinx.Ui.Helper; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; + +using GUI = Gtk.Builder.ObjectAttribute; + +namespace Ryujinx.Ui.Windows +{ + public class SettingsWindow : Window + { + private readonly MainWindow _parent; + private readonly ListStore _gameDirsBoxStore; + private readonly ListStore _audioBackendStore; + private readonly TimeZoneContentManager _timeZoneContentManager; + private readonly HashSet _validTzRegions; + + private long _systemTimeOffset; + +#pragma warning disable CS0649, IDE0044 + [GUI] CheckButton _errorLogToggle; + [GUI] CheckButton _warningLogToggle; + [GUI] CheckButton _infoLogToggle; + [GUI] CheckButton _stubLogToggle; + [GUI] CheckButton _debugLogToggle; + [GUI] CheckButton _fileLogToggle; + [GUI] CheckButton _guestLogToggle; + [GUI] CheckButton _fsAccessLogToggle; + [GUI] Adjustment _fsLogSpinAdjustment; + [GUI] ComboBoxText _graphicsDebugLevel; + [GUI] CheckButton _dockedModeToggle; + [GUI] CheckButton _discordToggle; + [GUI] CheckButton _checkUpdatesToggle; + [GUI] CheckButton _vSyncToggle; + [GUI] CheckButton _shaderCacheToggle; + [GUI] CheckButton _ptcToggle; + [GUI] CheckButton _fsicToggle; + [GUI] CheckButton _ignoreToggle; + [GUI] CheckButton _directKeyboardAccess; + [GUI] ComboBoxText _systemLanguageSelect; + [GUI] ComboBoxText _systemRegionSelect; + [GUI] Entry _systemTimeZoneEntry; + [GUI] EntryCompletion _systemTimeZoneCompletion; + [GUI] Box _audioBackendBox; + [GUI] ComboBox _audioBackendSelect; + [GUI] SpinButton _systemTimeYearSpin; + [GUI] SpinButton _systemTimeMonthSpin; + [GUI] SpinButton _systemTimeDaySpin; + [GUI] SpinButton _systemTimeHourSpin; + [GUI] SpinButton _systemTimeMinuteSpin; + [GUI] Adjustment _systemTimeYearSpinAdjustment; + [GUI] Adjustment _systemTimeMonthSpinAdjustment; + [GUI] Adjustment _systemTimeDaySpinAdjustment; + [GUI] Adjustment _systemTimeHourSpinAdjustment; + [GUI] Adjustment _systemTimeMinuteSpinAdjustment; + [GUI] CheckButton _custThemeToggle; + [GUI] Entry _custThemePath; + [GUI] ToggleButton _browseThemePath; + [GUI] Label _custThemePathLabel; + [GUI] TreeView _gameDirsBox; + [GUI] Entry _addGameDirBox; + [GUI] Entry _graphicsShadersDumpPath; + [GUI] ComboBoxText _anisotropy; + [GUI] ComboBoxText _aspectRatio; + [GUI] ComboBoxText _resScaleCombo; + [GUI] Entry _resScaleText; + [GUI] ToggleButton _configureController1; + [GUI] ToggleButton _configureController2; + [GUI] ToggleButton _configureController3; + [GUI] ToggleButton _configureController4; + [GUI] ToggleButton _configureController5; + [GUI] ToggleButton _configureController6; + [GUI] ToggleButton _configureController7; + [GUI] ToggleButton _configureController8; + [GUI] ToggleButton _configureControllerH; + +#pragma warning restore CS0649, IDE0044 + + public SettingsWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, HLE.FileSystem.Content.ContentManager contentManager) : this(parent, new Builder("Ryujinx.Ui.Windows.SettingsWindow.glade"), virtualFileSystem, contentManager) { } + + private SettingsWindow(MainWindow parent, Builder builder, VirtualFileSystem virtualFileSystem, HLE.FileSystem.Content.ContentManager contentManager) : base(builder.GetObject("_settingsWin").Handle) + { + _parent = parent; + + builder.Autoconnect(this); + + _timeZoneContentManager = new TimeZoneContentManager(); + _timeZoneContentManager.InitializeInstance(virtualFileSystem, contentManager, LibHac.FsSystem.IntegrityCheckLevel.None); + + _validTzRegions = new HashSet(_timeZoneContentManager.LocationNameCache.Length, StringComparer.Ordinal); // Zone regions are identifiers. Must match exactly. + + // Bind Events. + _configureController1.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player1); + _configureController2.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player2); + _configureController3.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player3); + _configureController4.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player4); + _configureController5.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player5); + _configureController6.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player6); + _configureController7.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player7); + _configureController8.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player8); + _configureControllerH.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Handheld); + _systemTimeZoneEntry.FocusOutEvent += TimeZoneEntry_FocusOut; + + _resScaleCombo.Changed += (sender, args) => _resScaleText.Visible = _resScaleCombo.ActiveId == "-1"; + + // Setup Currents. + if (ConfigurationState.Instance.Logger.EnableFileLog) + { + _fileLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableError) + { + _errorLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableWarn) + { + _warningLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableInfo) + { + _infoLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableStub) + { + _stubLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableDebug) + { + _debugLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableGuest) + { + _guestLogToggle.Click(); + } + + if (ConfigurationState.Instance.Logger.EnableFsAccessLog) + { + _fsAccessLogToggle.Click(); + } + + foreach (GraphicsDebugLevel level in Enum.GetValues(typeof(GraphicsDebugLevel))) + { + _graphicsDebugLevel.Append(level.ToString(), level.ToString()); + } + + _graphicsDebugLevel.SetActiveId(ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value.ToString()); + + if (ConfigurationState.Instance.System.EnableDockedMode) + { + _dockedModeToggle.Click(); + } + + if (ConfigurationState.Instance.EnableDiscordIntegration) + { + _discordToggle.Click(); + } + + if (ConfigurationState.Instance.CheckUpdatesOnStart) + { + _checkUpdatesToggle.Click(); + } + + if (ConfigurationState.Instance.Graphics.EnableVsync) + { + _vSyncToggle.Click(); + } + + if (ConfigurationState.Instance.Graphics.EnableShaderCache) + { + _shaderCacheToggle.Click(); + } + + if (ConfigurationState.Instance.System.EnablePtc) + { + _ptcToggle.Click(); + } + + if (ConfigurationState.Instance.System.EnableFsIntegrityChecks) + { + _fsicToggle.Click(); + } + + if (ConfigurationState.Instance.System.IgnoreMissingServices) + { + _ignoreToggle.Click(); + } + + if (ConfigurationState.Instance.Hid.EnableKeyboard) + { + _directKeyboardAccess.Click(); + } + + if (ConfigurationState.Instance.Ui.EnableCustomTheme) + { + _custThemeToggle.Click(); + } + + // Custom EntryCompletion Columns. If added to glade, need to override more signals + ListStore tzList = new ListStore(typeof(string), typeof(string), typeof(string)); + _systemTimeZoneCompletion.Model = tzList; + + CellRendererText offsetCol = new CellRendererText(); + CellRendererText abbrevCol = new CellRendererText(); + + _systemTimeZoneCompletion.PackStart(offsetCol, false); + _systemTimeZoneCompletion.AddAttribute(offsetCol, "text", 0); + _systemTimeZoneCompletion.TextColumn = 1; // Regions Column + _systemTimeZoneCompletion.PackStart(abbrevCol, false); + _systemTimeZoneCompletion.AddAttribute(abbrevCol, "text", 2); + + int maxLocationLength = 0; + + foreach (var (offset, location, abbr) in _timeZoneContentManager.ParseTzOffsets()) + { + var hours = Math.DivRem(offset, 3600, out int seconds); + var minutes = Math.Abs(seconds) / 60; + + var abbr2 = (abbr.StartsWith('+') || abbr.StartsWith('-')) ? string.Empty : abbr; + + tzList.AppendValues($"UTC{hours:+0#;-0#;+00}:{minutes:D2} ", location, abbr2); + _validTzRegions.Add(location); + + maxLocationLength = Math.Max(maxLocationLength, location.Length); + } + + _systemTimeZoneEntry.WidthChars = Math.Max(20, maxLocationLength + 1); // Ensure minimum Entry width + _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(); + + _systemTimeZoneCompletion.MatchFunc = TimeZoneMatchFunc; + + _systemLanguageSelect.SetActiveId(ConfigurationState.Instance.System.Language.Value.ToString()); + _systemRegionSelect.SetActiveId(ConfigurationState.Instance.System.Region.Value.ToString()); + _resScaleCombo.SetActiveId(ConfigurationState.Instance.Graphics.ResScale.Value.ToString()); + _anisotropy.SetActiveId(ConfigurationState.Instance.Graphics.MaxAnisotropy.Value.ToString()); + _aspectRatio.SetActiveId(((int)ConfigurationState.Instance.Graphics.AspectRatio.Value).ToString()); + + _custThemePath.Buffer.Text = ConfigurationState.Instance.Ui.CustomThemePath; + _resScaleText.Buffer.Text = ConfigurationState.Instance.Graphics.ResScaleCustom.Value.ToString(); + _resScaleText.Visible = _resScaleCombo.ActiveId == "-1"; + _graphicsShadersDumpPath.Buffer.Text = ConfigurationState.Instance.Graphics.ShadersDumpPath; + _fsLogSpinAdjustment.Value = ConfigurationState.Instance.System.FsGlobalAccessLogMode; + _systemTimeOffset = ConfigurationState.Instance.System.SystemTimeOffset; + + _gameDirsBox.AppendColumn("", new CellRendererText(), "text", 0); + _gameDirsBoxStore = new ListStore(typeof(string)); + _gameDirsBox.Model = _gameDirsBoxStore; + + foreach (string gameDir in ConfigurationState.Instance.Ui.GameDirs.Value) + { + _gameDirsBoxStore.AppendValues(gameDir); + } + + if (_custThemeToggle.Active == false) + { + _custThemePath.Sensitive = false; + _custThemePathLabel.Sensitive = false; + _browseThemePath.Sensitive = false; + } + + //Setup system time spinners + UpdateSystemTimeSpinners(); + + _audioBackendStore = new ListStore(typeof(string), typeof(AudioBackend)); + + TreeIter openAlIter = _audioBackendStore.AppendValues("OpenAL", AudioBackend.OpenAl); + TreeIter soundIoIter = _audioBackendStore.AppendValues("SoundIO", AudioBackend.SoundIo); + TreeIter dummyIter = _audioBackendStore.AppendValues("Dummy", AudioBackend.Dummy); + + _audioBackendSelect = ComboBox.NewWithModelAndEntry(_audioBackendStore); + _audioBackendSelect.EntryTextColumn = 0; + _audioBackendSelect.Entry.IsEditable = false; + + switch (ConfigurationState.Instance.System.AudioBackend.Value) + { + case AudioBackend.OpenAl: + _audioBackendSelect.SetActiveIter(openAlIter); + break; + case AudioBackend.SoundIo: + _audioBackendSelect.SetActiveIter(soundIoIter); + break; + case AudioBackend.Dummy: + _audioBackendSelect.SetActiveIter(dummyIter); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + _audioBackendBox.Add(_audioBackendSelect); + _audioBackendSelect.Show(); + + bool openAlIsSupported = false; + bool soundIoIsSupported = false; + + Task.Run(() => + { + openAlIsSupported = OpenALAudioOut.IsSupported; + soundIoIsSupported = SoundIoAudioOut.IsSupported; + }); + + // This function runs whenever the dropdown is opened + _audioBackendSelect.SetCellDataFunc(_audioBackendSelect.Cells[0], (layout, cell, model, iter) => + { + cell.Sensitive = ((AudioBackend)_audioBackendStore.GetValue(iter, 1)) switch + { + AudioBackend.OpenAl => openAlIsSupported, + AudioBackend.SoundIo => soundIoIsSupported, + AudioBackend.Dummy => true, + _ => throw new ArgumentOutOfRangeException() + }; + }); + } + + private void UpdateSystemTimeSpinners() + { + //Bind system time events + _systemTimeYearSpin.ValueChanged -= SystemTimeSpin_ValueChanged; + _systemTimeMonthSpin.ValueChanged -= SystemTimeSpin_ValueChanged; + _systemTimeDaySpin.ValueChanged -= SystemTimeSpin_ValueChanged; + _systemTimeHourSpin.ValueChanged -= SystemTimeSpin_ValueChanged; + _systemTimeMinuteSpin.ValueChanged -= SystemTimeSpin_ValueChanged; + + //Apply actual system time + SystemTimeOffset to system time spin buttons + DateTime systemTime = DateTime.Now.AddSeconds(_systemTimeOffset); + + _systemTimeYearSpinAdjustment.Value = systemTime.Year; + _systemTimeMonthSpinAdjustment.Value = systemTime.Month; + _systemTimeDaySpinAdjustment.Value = systemTime.Day; + _systemTimeHourSpinAdjustment.Value = systemTime.Hour; + _systemTimeMinuteSpinAdjustment.Value = systemTime.Minute; + + //Format spin buttons text to include leading zeros + _systemTimeYearSpin.Text = systemTime.Year.ToString("0000"); + _systemTimeMonthSpin.Text = systemTime.Month.ToString("00"); + _systemTimeDaySpin.Text = systemTime.Day.ToString("00"); + _systemTimeHourSpin.Text = systemTime.Hour.ToString("00"); + _systemTimeMinuteSpin.Text = systemTime.Minute.ToString("00"); + + //Bind system time events + _systemTimeYearSpin.ValueChanged += SystemTimeSpin_ValueChanged; + _systemTimeMonthSpin.ValueChanged += SystemTimeSpin_ValueChanged; + _systemTimeDaySpin.ValueChanged += SystemTimeSpin_ValueChanged; + _systemTimeHourSpin.ValueChanged += SystemTimeSpin_ValueChanged; + _systemTimeMinuteSpin.ValueChanged += SystemTimeSpin_ValueChanged; + } + + private void SaveSettings() + { + List gameDirs = new List(); + + _gameDirsBoxStore.GetIterFirst(out TreeIter treeIter); + for (int i = 0; i < _gameDirsBoxStore.IterNChildren(); i++) + { + gameDirs.Add((string)_gameDirsBoxStore.GetValue(treeIter, 0)); + + _gameDirsBoxStore.IterNext(ref treeIter); + } + + if (!float.TryParse(_resScaleText.Buffer.Text, out float resScaleCustom) || resScaleCustom <= 0.0f) + { + resScaleCustom = 1.0f; + } + + if (_validTzRegions.Contains(_systemTimeZoneEntry.Text)) + { + ConfigurationState.Instance.System.TimeZone.Value = _systemTimeZoneEntry.Text; + } + + ConfigurationState.Instance.Logger.EnableError.Value = _errorLogToggle.Active; + ConfigurationState.Instance.Logger.EnableWarn.Value = _warningLogToggle.Active; + ConfigurationState.Instance.Logger.EnableInfo.Value = _infoLogToggle.Active; + ConfigurationState.Instance.Logger.EnableStub.Value = _stubLogToggle.Active; + ConfigurationState.Instance.Logger.EnableDebug.Value = _debugLogToggle.Active; + ConfigurationState.Instance.Logger.EnableGuest.Value = _guestLogToggle.Active; + ConfigurationState.Instance.Logger.EnableFsAccessLog.Value = _fsAccessLogToggle.Active; + ConfigurationState.Instance.Logger.EnableFileLog.Value = _fileLogToggle.Active; + ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value = Enum.Parse(_graphicsDebugLevel.ActiveId); + ConfigurationState.Instance.System.EnableDockedMode.Value = _dockedModeToggle.Active; + ConfigurationState.Instance.EnableDiscordIntegration.Value = _discordToggle.Active; + ConfigurationState.Instance.CheckUpdatesOnStart.Value = _checkUpdatesToggle.Active; + ConfigurationState.Instance.Graphics.EnableVsync.Value = _vSyncToggle.Active; + ConfigurationState.Instance.Graphics.EnableShaderCache.Value = _shaderCacheToggle.Active; + ConfigurationState.Instance.System.EnablePtc.Value = _ptcToggle.Active; + ConfigurationState.Instance.System.EnableFsIntegrityChecks.Value = _fsicToggle.Active; + ConfigurationState.Instance.System.IgnoreMissingServices.Value = _ignoreToggle.Active; + ConfigurationState.Instance.Hid.EnableKeyboard.Value = _directKeyboardAccess.Active; + ConfigurationState.Instance.Ui.EnableCustomTheme.Value = _custThemeToggle.Active; + ConfigurationState.Instance.System.Language.Value = Enum.Parse(_systemLanguageSelect.ActiveId); + ConfigurationState.Instance.System.Region.Value = Enum.Parse(_systemRegionSelect.ActiveId); + ConfigurationState.Instance.System.SystemTimeOffset.Value = _systemTimeOffset; + ConfigurationState.Instance.Ui.CustomThemePath.Value = _custThemePath.Buffer.Text; + ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = _graphicsShadersDumpPath.Buffer.Text; + ConfigurationState.Instance.Ui.GameDirs.Value = gameDirs; + ConfigurationState.Instance.System.FsGlobalAccessLogMode.Value = (int)_fsLogSpinAdjustment.Value; + ConfigurationState.Instance.Graphics.MaxAnisotropy.Value = float.Parse(_anisotropy.ActiveId, CultureInfo.InvariantCulture); + ConfigurationState.Instance.Graphics.AspectRatio.Value = Enum.Parse(_aspectRatio.ActiveId); + ConfigurationState.Instance.Graphics.ResScale.Value = int.Parse(_resScaleCombo.ActiveId); + ConfigurationState.Instance.Graphics.ResScaleCustom.Value = resScaleCustom; + + if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter)) + { + ConfigurationState.Instance.System.AudioBackend.Value = (AudioBackend)_audioBackendStore.GetValue(activeIter, 1); + } + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + _parent.UpdateGraphicsConfig(); + ThemeHelper.ApplyTheme(); + } + + // + // Events + // + private void TimeZoneEntry_FocusOut(object sender, FocusOutEventArgs e) + { + if (!_validTzRegions.Contains(_systemTimeZoneEntry.Text)) + { + _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(); + } + } + + private bool TimeZoneMatchFunc(EntryCompletion compl, string key, TreeIter iter) + { + key = key.Trim().Replace(' ', '_'); + + return ((string)compl.Model.GetValue(iter, 1)).Contains(key, StringComparison.OrdinalIgnoreCase) || // region + ((string)compl.Model.GetValue(iter, 2)).StartsWith(key, StringComparison.OrdinalIgnoreCase) || // abbr + ((string)compl.Model.GetValue(iter, 0))[3..].StartsWith(key); // offset + } + + private void SystemTimeSpin_ValueChanged(object sender, EventArgs e) + { + int year = _systemTimeYearSpin.ValueAsInt; + int month = _systemTimeMonthSpin.ValueAsInt; + int day = _systemTimeDaySpin.ValueAsInt; + int hour = _systemTimeHourSpin.ValueAsInt; + int minute = _systemTimeMinuteSpin.ValueAsInt; + + if (!DateTime.TryParse(year + "-" + month + "-" + day + " " + hour + ":" + minute, out DateTime newTime)) + { + UpdateSystemTimeSpinners(); + + return; + } + + newTime = newTime.AddSeconds(DateTime.Now.Second).AddMilliseconds(DateTime.Now.Millisecond); + + long systemTimeOffset = (long)Math.Ceiling((newTime - DateTime.Now).TotalMinutes) * 60L; + + if (_systemTimeOffset != systemTimeOffset) + { + _systemTimeOffset = systemTimeOffset; + UpdateSystemTimeSpinners(); + } + } + + private void AddDir_Pressed(object sender, EventArgs args) + { + if (Directory.Exists(_addGameDirBox.Buffer.Text)) + { + _gameDirsBoxStore.AppendValues(_addGameDirBox.Buffer.Text); + } + else + { + FileChooserDialog fileChooser = new FileChooserDialog("Choose the game directory to add to the list", this, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept) + { + SelectMultiple = true + }; + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + foreach (string directory in fileChooser.Filenames) + { + bool directoryAdded = false; + + if (_gameDirsBoxStore.GetIterFirst(out TreeIter treeIter)) + { + do + { + if (directory.Equals((string)_gameDirsBoxStore.GetValue(treeIter, 0))) + { + directoryAdded = true; + break; + } + } while(_gameDirsBoxStore.IterNext(ref treeIter)); + } + + if (!directoryAdded) + { + _gameDirsBoxStore.AppendValues(directory); + } + } + } + + fileChooser.Dispose(); + } + + _addGameDirBox.Buffer.Text = ""; + + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + } + + private void RemoveDir_Pressed(object sender, EventArgs args) + { + TreeSelection selection = _gameDirsBox.Selection; + + if (selection.GetSelected(out TreeIter treeIter)) + { + _gameDirsBoxStore.Remove(ref treeIter); + } + + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + } + + private void CustThemeToggle_Activated(object sender, EventArgs args) + { + _custThemePath.Sensitive = _custThemeToggle.Active; + _custThemePathLabel.Sensitive = _custThemeToggle.Active; + _browseThemePath.Sensitive = _custThemeToggle.Active; + } + + private void BrowseThemeDir_Pressed(object sender, EventArgs args) + { + using (FileChooserDialog fileChooser = new FileChooserDialog("Choose the theme to load", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Select", ResponseType.Accept)) + { + fileChooser.Filter = new FileFilter(); + fileChooser.Filter.AddPattern("*.css"); + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + _custThemePath.Buffer.Text = fileChooser.Filename; + } + } + + _browseThemePath.SetStateFlags(StateFlags.Normal, true); + } + + private void ConfigureController_Pressed(object sender, PlayerIndex playerIndex) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + new ControllerWindow(playerIndex).Show(); + } + + private void SaveToggle_Activated(object sender, EventArgs args) + { + SaveSettings(); + Dispose(); + } + + private void ApplyToggle_Activated(object sender, EventArgs args) + { + SaveSettings(); + } + + private void CloseToggle_Activated(object sender, EventArgs args) + { + Dispose(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Windows/SettingsWindow.glade b/Ryujinx/Ui/Windows/SettingsWindow.glade new file mode 100644 index 00000000..936aa520 --- /dev/null +++ b/Ryujinx/Ui/Windows/SettingsWindow.glade @@ -0,0 +1,2411 @@ + + + + + + 3 + 1 + 10 + + + 1 + 31 + 1 + 5 + + + 23 + 1 + 5 + + + 59 + 1 + 5 + + + 1 + 12 + 1 + 5 + + + 2000 + 2060 + 1 + 10 + + + 0 + True + True + + + False + Ryujinx - Settings + True + center + 650 + 550 + + + True + False + vertical + + + True + True + in + + + True + False + + + True + True + + + True + False + 5 + 10 + 5 + vertical + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + General + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + Enable Discord Rich Presence + True + True + False + Enables or disables Discord Rich Presence + start + True + + + False + True + 5 + 0 + + + + + Check for Updates on Launch + True + True + False + start + True + + + False + True + 5 + 1 + + + + + True + True + 1 + + + + + False + True + 5 + 1 + + + + + True + False + 5 + 5 + + + False + True + 5 + 2 + + + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + Game Directories + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + True + True + 10 + in + + + True + True + False + False + + + + + + + + + True + True + 0 + + + + + True + False + + + True + True + Enter a game directroy to add to the list + + + True + True + 0 + + + + + Add + 80 + True + True + True + Add a game directory to the list + 5 + + + + False + True + 1 + + + + + Remove + 80 + True + True + True + Remove selected game directory + 5 + + + + False + True + 3 + + + + + False + True + 1 + + + + + True + True + 1 + + + + + True + True + 5 + 4 + + + + + True + False + 5 + 5 + + + False + True + 5 + 5 + + + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + Themes + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + Use Custom Theme + True + True + False + Enable or disable custom themes in the GUI + start + True + + + + False + True + 5 + 1 + + + + + True + False + + + True + False + Path to custom GUI theme + Custom Theme Path: + + + False + True + 5 + 0 + + + + + True + True + Path to custom GUI theme + center + + + True + True + 1 + + + + + Browse... + 80 + True + True + True + Browse for a custom GUI theme + 5 + + + + False + True + 2 + + + + + False + True + 10 + 2 + + + + + False + True + 1 + + + + + False + True + 5 + 6 + + + + + + + True + False + General + + + False + + + + + True + False + 5 + 10 + 5 + vertical + + + True + False + 5 + 5 + + + Enable Docked Mode + True + True + False + Enable or disable Docked Mode + True + + + False + True + 10 + 0 + + + + + Direct Keyboard Access + True + True + False + Enable or disable "direct keyboard access (HID) support" (Provides games access to your keyboard as a text entry device) + True + + + False + False + 10 + 1 + + + + + False + True + 5 + 0 + + + + + True + False + + + False + True + 1 + + + + + + True + False + center + center + 20 + + + True + False + vertical + + + True + False + 20 + 20 + Player 1 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 0 + 0 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 3 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 4 + 0 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 2 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 2 + 0 + + + + + True + False + vertical + + + True + False + 20 + 20 + Handheld + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 4 + 4 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 6 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 4 + 2 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 5 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 2 + 2 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 7 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 0 + 4 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 4 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 0 + 2 + + + + + True + False + vertical + + + True + False + 20 + 20 + Player 8 + + + False + True + 0 + + + + + Configure + True + True + True + 20 + 20 + 20 + 20 + + + False + True + 1 + + + + + 2 + 4 + + + + + True + False + + + 1 + 0 + + + + + True + False + + + 3 + 0 + + + + + True + False + + + 3 + 2 + + + + + True + False + + + 3 + 4 + + + + + True + False + + + 1 + 2 + + + + + True + False + + + 1 + 4 + + + + + True + False + + + 1 + 1 + + + + + True + False + + + 1 + 3 + + + + + True + False + + + 3 + 1 + + + + + True + False + + + 3 + 3 + + + + + True + False + + + 0 + 1 + + + + + True + False + + + 2 + 1 + + + + + True + False + + + 4 + 1 + + + + + True + False + + + 0 + 3 + + + + + True + False + + + 2 + 3 + + + + + True + False + + + 4 + 3 + + + + + True + True + 2 + + + + + True + False + + + False + True + 3 + + + + + 1 + + + + + True + False + Input + + + 1 + False + + + + + True + False + 5 + 10 + 5 + vertical + + + True + False + start + 5 + 5 + vertical + + + True + False + start + 5 + Core + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + True + False + + + True + False + Change System Region + end + System Region: + + + False + True + 5 + 2 + + + + + True + False + Change System Region + 5 + + Japan + USA + Europe + Australia + China + Korea + Taiwan + + + + False + True + 3 + + + + + False + True + 5 + 0 + + + + + True + False + + + True + False + Change System Language + end + System Language: + + + False + True + 5 + 0 + + + + + True + False + Change System Language + + American English + British English + Canadian French + Chinese + Dutch + French + German + Italian + Japanese + Korean + Latin American Spanish + Portuguese + Russian + Simplified Chinese + Spanish + Taiwanese + Traditional Chinese + + + + False + True + 1 + + + + + False + True + 5 + 1 + + + + + True + False + + + True + False + Change System TimeZone + end + System TimeZone: + + + False + True + 5 + 1 + + + + + True + True + Change System TimeZone + 5 + _systemTimeZoneCompletion + + + False + True + 2 + + + + + False + True + 5 + 2 + + + + + True + False + + + True + False + end + System Time: + + + False + True + 5 + 0 + + + + + True + True + 2000 + vertical + _systemTimeYearSpinAdjustment + True + 2000 + + + False + True + 1 + + + + + True + False + end + - + + + False + True + 5 + 2 + + + + + True + True + 1 + vertical + _systemTimeMonthSpinAdjustment + True + 1 + + + False + True + 3 + + + + + True + False + end + - + + + False + True + 5 + 4 + + + + + True + True + 1 + vertical + _systemTimeDaySpinAdjustment + True + 1 + + + False + True + 5 + + + + + True + True + 0 + vertical + _systemTimeHourSpinAdjustment + True + + + False + True + 6 + + + + + True + False + end + : + + + False + True + 5 + 7 + + + + + True + True + 0 + vertical + _systemTimeMinuteSpinAdjustment + True + + + False + True + 8 + + + + + False + True + 5 + 3 + + + + + Enable VSync + True + True + False + Enables or disables Vertical Sync + start + 5 + 5 + True + + + False + True + 4 + + + + + Enable Profiled Persistent Translation Cache + True + True + False + Enables or disables profiled translation cache persistency + start + 5 + 5 + True + + + False + True + 6 + + + + + Enable FS Integrity Checks + True + True + False + Enables integrity checks on Game content files + start + 5 + 5 + True + + + False + True + 7 + + + + + True + True + 1 + + + + + True + False + + + + + + True + False + Change System Region + end + 5 + Audio Backend: + + + False + True + 5 + 2 + + + + + False + True + 5 + 2 + + + + + False + True + 5 + 0 + + + + + True + False + 5 + 5 + + + False + True + 5 + 1 + + + + + True + False + start + 5 + 5 + vertical + + + True + False + + + True + False + start + 5 + Hacks + + + + + + False + True + 0 + + + + + True + False + start + 5 + - These may cause instability + + + False + True + 1 + + + + + False + True + 1 + + + + + True + False + 10 + 10 + vertical + + + Ignore Missing Services + True + True + False + Enable or disable ignoring missing services + start + 5 + 5 + True + + + False + True + 0 + + + + + True + True + 2 + + + + + False + True + 5 + 4 + + + + + 2 + + + + + True + False + end + System + + + 2 + False + + + + + True + False + 5 + vertical + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + 5 + 5 + Enhancements + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + Enable Shader Cache + True + True + False + Enables or disables Shader Cache + start + 5 + 5 + True + + + False + True + 0 + + + + + True + False + 5 + 5 + + + True + False + Resolution Scale applied to applicable render targets. + Resolution Scale: + + + False + True + 5 + 0 + + + + + True + False + Resolution Scale applied to applicable render targets. + 1 + + Native (720p/1080p) + 2x (1440p/2160p) + 3x (2160p/3240p) + 4x (2880p/4320p) + Custom (not recommended) + + + + False + True + 1 + + + + + True + True + Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash. + center + False + 1.0 + number + + + True + True + 2 + + + + + False + True + 5 + 1 + + + + + True + False + 5 + 5 + + + True + False + Level of Anisotropic Filtering (set to Auto to use the value requested by the game) + Anisotropic Filtering: + + + False + True + 5 + 0 + + + + + True + False + Level of Anisotropic Filtering (set to Auto to use the value requested by the game) + -1 + + Auto + 2x + 4x + 8x + 16x + + + + False + True + 1 + + + + + False + True + 5 + 1 + + + + + True + False + 5 + 5 + + + True + False + Aspect Ratio applied to the renderer window. + Aspect Ratio: + + + False + True + 5 + 0 + + + + + True + False + Aspect Ratio applied to the renderer window. + 1 + + 4:3 + 16:9 + 16:10 + 21:9 + 32:9 + Stretch to Fit Window + + + + False + True + 1 + + + + + False + True + 5 + 3 + + + + + False + True + 2 + + + + + False + True + 5 + 0 + + + + + True + False + + + False + True + 5 + 1 + + + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + 5 + 5 + Developer Options + + + + + + False + True + 0 + + + + + True + False + 10 + 10 + vertical + + + True + False + 5 + 5 + + + True + False + Graphics Shaders Dump Path + Graphics Shaders Dump Path: + + + False + True + 5 + 0 + + + + + True + True + Graphics Shaders Dump Path + center + False + + + True + True + 1 + + + + + False + True + 5 + 0 + + + + + False + True + 1 + + + + + False + True + 5 + 4 + + + + + 3 + + + + + True + False + Graphics + + + 3 + False + + + + + True + False + 5 + 10 + 5 + vertical + + + True + False + 5 + 5 + vertical + + + True + False + start + 5 + Logging + + + + + + False + True + 0 + + + + + True + False + start + 10 + 10 + vertical + + + Enable Logging to File + True + True + False + Enables or disables logging to a file on disk + start + 5 + 5 + True + + + False + True + 0 + + + + + Enable Stub Logs + True + True + False + Enables printing stub log messages + start + 5 + 5 + True + + + False + True + 3 + + + + + Enable Info Logs + True + True + False + Enables printing info log messages + start + 5 + 5 + True + + + False + True + 4 + + + + + Enable Warning Logs + True + True + False + Enables printing warning log messages + start + 5 + 5 + True + + + False + True + 5 + + + + + Enable Error Logs + True + True + False + Enables printing error log messages + start + 5 + 5 + True + + + False + True + 6 + + + + + Enable Guest Logs + True + True + False + Enables printing guest log messages + start + 5 + 5 + True + + + False + True + 7 + + + + + Enable Fs Access Logs + True + True + False + Enables printing fs access log messages + start + 5 + 5 + True + + + False + True + 8 + + + + + True + False + + + True + False + Enables FS access log output to the console. Possible modes are 0-3 + Fs Global Access Log Mode: + + + False + True + 5 + 0 + + + + + True + True + Enables FS access log output to the console. Possible modes are 0-3 + 0 + _fsLogSpinAdjustment + + + True + True + 1 + + + + + False + True + 5 + 9 + + + + + True + True + 1 + + + + + False + True + 5 + 0 + + + + + True + False + 5 + 5 + 10 + vertical + + + True + False + Use with care + start + 5 + Developer Options (WARNING: Will reduce performance) + + + + + + False + True + 0 + + + + + True + False + start + 10 + 10 + vertical + + + True + False + 5 + + + True + False + Requires appropriate log levels enabled. + OpenGL Log Level + + + False + True + 5 + 22 + + + + + True + False + Requires appropriate log levels enabled. + 5 + + + False + True + 22 + + + + + False + True + 1 + + + + + Enable Debug Logs + True + True + False + Enables printing debug log messages + start + 5 + 5 + True + + + False + True + 21 + + + + + False + True + 1 + + + + + False + True + 5 + 22 + + + + + 4 + + + + + True + False + Logging + + + 4 + False + + + + + + + + + True + True + 0 + + + + + True + False + 5 + 3 + 3 + 5 + end + + + Save + True + True + True + + + + False + False + 0 + + + + + Close + True + True + True + + + + False + False + 1 + + + + + Apply + True + True + True + + + + True + True + 2 + + + + + False + False + 1 + + + + + + diff --git a/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/Ryujinx/Ui/Windows/TitleUpdateWindow.cs new file mode 100644 index 00000000..647dea19 --- /dev/null +++ b/Ryujinx/Ui/Windows/TitleUpdateWindow.cs @@ -0,0 +1,202 @@ +using Gtk; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; +using LibHac.Ns; +using Ryujinx.Common.Configuration; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.Ui.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +using GUI = Gtk.Builder.ObjectAttribute; +using JsonHelper = Ryujinx.Common.Utilities.JsonHelper; + +namespace Ryujinx.Ui.Windows +{ + public class TitleUpdateWindow : Window + { + private readonly MainWindow _parent; + private readonly VirtualFileSystem _virtualFileSystem; + private readonly string _titleId; + private readonly string _updateJsonPath; + + private TitleUpdateMetadata _titleUpdateWindowData; + + private readonly Dictionary _radioButtonToPathDictionary; + +#pragma warning disable CS0649, IDE0044 + [GUI] Label _baseTitleInfoLabel; + [GUI] Box _availableUpdatesBox; + [GUI] RadioButton _noUpdateRadioButton; +#pragma warning restore CS0649, IDE0044 + + public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } + + private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetObject("_titleUpdateWindow").Handle) + { + _parent = parent; + + builder.Autoconnect(this); + + _titleId = titleId; + _virtualFileSystem = virtualFileSystem; + _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json"); + _radioButtonToPathDictionary = new Dictionary(); + + try + { + _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath); + } + catch + { + _titleUpdateWindowData = new TitleUpdateMetadata + { + Selected = "", + Paths = new List() + }; + } + + _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]"; + + foreach (string path in _titleUpdateWindowData.Paths) + { + AddUpdate(path); + } + + if (_titleUpdateWindowData.Selected == "") + { + _noUpdateRadioButton.Active = true; + } + else + { + foreach ((RadioButton update, var _) in _radioButtonToPathDictionary.Where(keyValuePair => keyValuePair.Value == _titleUpdateWindowData.Selected)) + { + update.Active = true; + } + } + } + + private void AddUpdate(string path) + { + if (File.Exists(path)) + { + using (FileStream file = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage()); + + try + { + (Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); + + if (controlNca != null && patchNca != null) + { + ApplicationControlProperty controlData = new ApplicationControlProperty(); + + controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); + + RadioButton radioButton = new RadioButton($"Version {controlData.DisplayVersion.ToString()} - {path}"); + radioButton.JoinGroup(_noUpdateRadioButton); + + _availableUpdatesBox.Add(radioButton); + _radioButtonToPathDictionary.Add(radioButton, path); + + radioButton.Show(); + radioButton.Active = true; + } + else + { + GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); + } + } + catch (Exception exception) + { + GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}"); + } + } + } + } + + private void RemoveUpdates(bool removeSelectedOnly = false) + { + foreach (RadioButton radioButton in _noUpdateRadioButton.Group) + { + if (radioButton.Label != "No Update" && (!removeSelectedOnly || radioButton.Active)) + { + _availableUpdatesBox.Remove(radioButton); + _radioButtonToPathDictionary.Remove(radioButton); + radioButton.Dispose(); + } + } + } + + private void AddButton_Clicked(object sender, EventArgs args) + { + using (FileChooserDialog fileChooser = new FileChooserDialog("Select update files", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept)) + { + fileChooser.SelectMultiple = true; + fileChooser.SetPosition(WindowPosition.Center); + fileChooser.Filter = new FileFilter(); + fileChooser.Filter.AddPattern("*.nsp"); + + if (fileChooser.Run() == (int)ResponseType.Accept) + { + foreach (string path in fileChooser.Filenames) + { + AddUpdate(path); + } + } + } + } + + private void RemoveButton_Clicked(object sender, EventArgs args) + { + RemoveUpdates(true); + } + + private void RemoveAllButton_Clicked(object sender, EventArgs args) + { + RemoveUpdates(); + } + + private void SaveButton_Clicked(object sender, EventArgs args) + { + _titleUpdateWindowData.Paths.Clear(); + _titleUpdateWindowData.Selected = ""; + + foreach (string paths in _radioButtonToPathDictionary.Values) + { + _titleUpdateWindowData.Paths.Add(paths); + } + + foreach (RadioButton radioButton in _noUpdateRadioButton.Group) + { + if (radioButton.Active) + { + _titleUpdateWindowData.Selected = _radioButtonToPathDictionary.TryGetValue(radioButton, out string updatePath) ? updatePath : ""; + } + } + + using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough)) + { + dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true))); + } + + _parent.UpdateGameTable(); + + Dispose(); + } + + private void CancelButton_Clicked(object sender, EventArgs args) + { + Dispose(); + } + } +} \ No newline at end of file diff --git a/Ryujinx/Ui/Windows/TitleUpdateWindow.glade b/Ryujinx/Ui/Windows/TitleUpdateWindow.glade new file mode 100644 index 00000000..de557471 --- /dev/null +++ b/Ryujinx/Ui/Windows/TitleUpdateWindow.glade @@ -0,0 +1,214 @@ + + + + + + False + Ryujinx - Title Update Manager + True + center + 550 + 250 + + + True + False + vertical + + + True + False + vertical + + + True + False + 10 + 10 + 10 + 10 + Available Updates + + + False + True + 0 + + + + + True + True + 10 + 10 + in + + + True + False + + + True + False + vertical + + + No Update + True + True + False + True + True + + + False + True + 0 + + + + + + + + + True + True + 1 + + + + + True + True + 0 + + + + + True + False + + + True + False + 10 + 10 + start + + + Add + True + True + True + Adds an update to this list + 10 + + + + True + True + 0 + + + + + Remove + True + True + True + Removes the selected update + 10 + + + + True + True + 1 + + + + + Remove All + True + True + True + Removes the selected update + 10 + + + + True + True + 2 + + + + + True + True + 0 + + + + + True + False + 10 + 10 + end + + + Save + True + True + True + 10 + 2 + 2 + + + + True + True + 0 + + + + + Cancel + True + True + True + 10 + 2 + 2 + + + + True + True + 1 + + + + + True + True + 1 + + + + + False + True + 1 + + + + + + + + + diff --git a/Ryujinx/Ui/assets/DiscordLogo.png b/Ryujinx/Ui/assets/DiscordLogo.png deleted file mode 100644 index f3486b99..00000000 Binary files a/Ryujinx/Ui/assets/DiscordLogo.png and /dev/null differ diff --git a/Ryujinx/Ui/assets/GitHubLogo.png b/Ryujinx/Ui/assets/GitHubLogo.png deleted file mode 100644 index 2e860709..00000000 Binary files a/Ryujinx/Ui/assets/GitHubLogo.png and /dev/null differ diff --git a/Ryujinx/Ui/assets/Icon.png b/Ryujinx/Ui/assets/Icon.png deleted file mode 100644 index 2fc7b017..00000000 Binary files a/Ryujinx/Ui/assets/Icon.png and /dev/null differ diff --git a/Ryujinx/Ui/assets/JoyConLeft.svg b/Ryujinx/Ui/assets/JoyConLeft.svg deleted file mode 100644 index 40d06136..00000000 --- a/Ryujinx/Ui/assets/JoyConLeft.svg +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Ryujinx/Ui/assets/JoyConPair.svg b/Ryujinx/Ui/assets/JoyConPair.svg deleted file mode 100644 index fca94d18..00000000 --- a/Ryujinx/Ui/assets/JoyConPair.svg +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Ryujinx/Ui/assets/JoyConRight.svg b/Ryujinx/Ui/assets/JoyConRight.svg deleted file mode 100644 index 014c0ae3..00000000 --- a/Ryujinx/Ui/assets/JoyConRight.svg +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Ryujinx/Ui/assets/NCAIcon.png b/Ryujinx/Ui/assets/NCAIcon.png deleted file mode 100644 index 6d73c8c7..00000000 Binary files a/Ryujinx/Ui/assets/NCAIcon.png and /dev/null differ diff --git a/Ryujinx/Ui/assets/NROIcon.png b/Ryujinx/Ui/assets/NROIcon.png deleted file mode 100644 index bc6b65bf..00000000 Binary files a/Ryujinx/Ui/assets/NROIcon.png and /dev/null differ diff --git a/Ryujinx/Ui/assets/NSOIcon.png b/Ryujinx/Ui/assets/NSOIcon.png deleted file mode 100644 index 8782b3ea..00000000 Binary files a/Ryujinx/Ui/assets/NSOIcon.png and /dev/null differ diff --git a/Ryujinx/Ui/assets/NSPIcon.png b/Ryujinx/Ui/assets/NSPIcon.png deleted file mode 100644 index d01dc482..00000000 Binary files a/Ryujinx/Ui/assets/NSPIcon.png and /dev/null differ diff --git a/Ryujinx/Ui/assets/PatreonLogo.png b/Ryujinx/Ui/assets/PatreonLogo.png deleted file mode 100644 index 19c7ffbc..00000000 Binary files a/Ryujinx/Ui/assets/PatreonLogo.png and /dev/null differ diff --git a/Ryujinx/Ui/assets/ProCon.svg b/Ryujinx/Ui/assets/ProCon.svg deleted file mode 100644 index 8c2b879f..00000000 --- a/Ryujinx/Ui/assets/ProCon.svg +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Ryujinx/Ui/assets/TwitterLogo.png b/Ryujinx/Ui/assets/TwitterLogo.png deleted file mode 100644 index 3d01efc0..00000000 Binary files a/Ryujinx/Ui/assets/TwitterLogo.png and /dev/null differ diff --git a/Ryujinx/Ui/assets/XCIIcon.png b/Ryujinx/Ui/assets/XCIIcon.png deleted file mode 100644 index 08f783a8..00000000 Binary files a/Ryujinx/Ui/assets/XCIIcon.png and /dev/null differ diff --git a/Ryujinx/Updater/UpdateDialog.cs b/Ryujinx/Updater/UpdateDialog.cs deleted file mode 100644 index ed49ad4a..00000000 --- a/Ryujinx/Updater/UpdateDialog.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Gdk; -using Gtk; -using Mono.Unix; -using System; -using System.Diagnostics; -using System.Linq; -using System.Runtime.InteropServices; - -namespace Ryujinx.Ui -{ - public class UpdateDialog : Gtk.Window - { -#pragma warning disable CS0649, IDE0044 - [Builder.Object] public Label MainText; - [Builder.Object] public Label SecondaryText; - [Builder.Object] public LevelBar ProgressBar; - [Builder.Object] public Button YesButton; - [Builder.Object] public Button NoButton; -#pragma warning restore CS0649, IDE0044 - - private readonly MainWindow _mainWindow; - private readonly string _buildUrl; - private bool _restartQuery; - - public UpdateDialog(MainWindow mainWindow, Version newVersion, string buildUrl) : this(new Builder("Ryujinx.Updater.UpdateDialog.glade"), mainWindow, newVersion, buildUrl) { } - - private UpdateDialog(Builder builder, MainWindow mainWindow, Version newVersion, string buildUrl) : base(builder.GetObject("UpdateDialog").Handle) - { - builder.Autoconnect(this); - - _mainWindow = mainWindow; - _buildUrl = buildUrl; - - MainText.Text = "Do you want to update Ryujinx to the latest version?"; - SecondaryText.Text = $"{Program.Version} -> {newVersion}"; - - ProgressBar.Hide(); - - YesButton.Clicked += YesButton_Pressed; - NoButton.Clicked += NoButton_Pressed; - } - - private void YesButton_Pressed(object sender, EventArgs args) - { - if (_restartQuery) - { - string ryuName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Ryujinx.exe" : "Ryujinx"; - string ryuExe = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ryuName); - string ryuArg = String.Join(" ", Environment.GetCommandLineArgs().AsEnumerable().Skip(1).ToArray()); - - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - UnixFileInfo unixFileInfo = new UnixFileInfo(ryuExe); - unixFileInfo.FileAccessPermissions |= FileAccessPermissions.UserExecute; - } - - Process.Start(ryuExe, ryuArg); - - Environment.Exit(0); - } - else - { - this.Window.Functions = _mainWindow.Window.Functions = WMFunction.All & WMFunction.Close; - _mainWindow.ExitMenuItem.Sensitive = false; - - YesButton.Hide(); - NoButton.Hide(); - ProgressBar.Show(); - - SecondaryText.Text = ""; - _restartQuery = true; - - _ = Updater.UpdateRyujinx(this, _buildUrl); - } - } - - private void NoButton_Pressed(object sender, EventArgs args) - { - Updater.Running = false; - _mainWindow.Window.Functions = WMFunction.All; - - _mainWindow.ExitMenuItem.Sensitive = true; - _mainWindow.UpdateMenuItem.Sensitive = true; - - this.Dispose(); - } - } -} diff --git a/Ryujinx/Updater/UpdateDialog.glade b/Ryujinx/Updater/UpdateDialog.glade deleted file mode 100644 index cc80167e..00000000 --- a/Ryujinx/Updater/UpdateDialog.glade +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - False - Ryujinx - Updater - False - center - 400 - 130 - - - - - - True - False - 10 - 10 - 10 - 10 - vertical - - - True - False - vertical - - - True - False - 5 - 5 - - - - - - - False - True - 0 - - - - - True - False - 5 - 5 - - - False - True - 1 - - - - - 20 - True - False - 5 - 5 - 100 - - - False - True - 2 - - - - - True - True - 0 - - - - - True - False - - - Yes - True - True - True - 5 - 5 - 5 - - - True - True - 0 - - - - - No - True - True - True - 5 - 5 - 5 - - - True - True - 1 - - - - - False - True - 1 - - - - - - diff --git a/Ryujinx/Updater/Updater.cs b/Ryujinx/Updater/Updater.cs deleted file mode 100644 index 42b97cc8..00000000 --- a/Ryujinx/Updater/Updater.cs +++ /dev/null @@ -1,359 +0,0 @@ -using Gtk; -using ICSharpCode.SharpZipLib.GZip; -using ICSharpCode.SharpZipLib.Tar; -using ICSharpCode.SharpZipLib.Zip; -using Newtonsoft.Json.Linq; -using Ryujinx.Common.Logging; -using Ryujinx.Ui; -using System; -using System.IO; -using System.Net; -using System.Net.NetworkInformation; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -namespace Ryujinx -{ - public static class Updater - { - internal static bool Running; - - private static readonly string HomeDir = AppDomain.CurrentDomain.BaseDirectory; - private static readonly string UpdateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update"); - private static readonly string UpdatePublishDir = Path.Combine(UpdateDir, "publish"); - - private static string _jobId; - private static string _buildVer; - private static string _platformExt; - private static string _buildUrl; - - private const string AppveyorApiUrl = "https://ci.appveyor.com/api"; - - public static async Task BeginParse(MainWindow mainWindow, bool showVersionUpToDate) - { - if (Running) return; - - Running = true; - mainWindow.UpdateMenuItem.Sensitive = false; - - // Detect current platform - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - _platformExt = "osx_x64.zip"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - _platformExt = "win_x64.zip"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - _platformExt = "linux_x64.tar.gz"; - } - - Version newVersion; - Version currentVersion; - - try - { - currentVersion = Version.Parse(Program.Version); - } - catch - { - GtkDialog.CreateWarningDialog("Failed to convert the current Ryujinx version.", "Cancelling Update!"); - Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!"); - - return; - } - - // Get latest version number from Appveyor - try - { - using (WebClient jsonClient = new WebClient()) - { - string fetchedJson = await jsonClient.DownloadStringTaskAsync($"{AppveyorApiUrl}/projects/gdkchan/ryujinx/branch/master"); - JObject jsonRoot = JObject.Parse(fetchedJson); - JToken buildToken = jsonRoot["build"]; - - _jobId = (string)buildToken["jobs"][0]["jobId"]; - _buildVer = (string)buildToken["version"]; - _buildUrl = $"{AppveyorApiUrl}/buildjobs/{_jobId}/artifacts/ryujinx-{_buildVer}-{_platformExt}"; - - // If build not done, assume no new update are availaible. - if ((string)buildToken["jobs"][0]["status"] != "success") - { - if (showVersionUpToDate) - { - GtkDialog.CreateInfoDialog("Ryujinx - Updater", "You are already using the most updated version of Ryujinx!", ""); - } - - return; - } - } - } - catch (Exception exception) - { - Logger.Error?.Print(LogClass.Application, exception.Message); - GtkDialog.CreateErrorDialog("An error has occurred when trying to get release information from AppVeyor."); - - return; - } - - try - { - newVersion = Version.Parse(_buildVer); - } - catch - { - GtkDialog.CreateWarningDialog("Failed to convert the received Ryujinx version from AppVeyor.", "Cancelling Update!"); - Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from AppVeyor!"); - - return; - } - - if (newVersion <= currentVersion) - { - if (showVersionUpToDate) - { - GtkDialog.CreateInfoDialog("Ryujinx - Updater", "You are already using the most updated version of Ryujinx!", ""); - } - - Running = false; - mainWindow.UpdateMenuItem.Sensitive = true; - - return; - } - - // Show a message asking the user if they want to update - UpdateDialog updateDialog = new UpdateDialog(mainWindow, newVersion, _buildUrl); - updateDialog.Show(); - } - - public static async Task UpdateRyujinx(UpdateDialog updateDialog, string downloadUrl) - { - // Empty update dir, although it shouldn't ever have anything inside it - if (Directory.Exists(UpdateDir)) - { - Directory.Delete(UpdateDir, true); - } - - Directory.CreateDirectory(UpdateDir); - - string updateFile = Path.Combine(UpdateDir, "update.bin"); - - // Download the update .zip - updateDialog.MainText.Text = "Downloading Update..."; - updateDialog.ProgressBar.Value = 0; - updateDialog.ProgressBar.MaxValue = 100; - - using (WebClient client = new WebClient()) - { - client.DownloadProgressChanged += (_, args) => - { - updateDialog.ProgressBar.Value = args.ProgressPercentage; - }; - - await client.DownloadFileTaskAsync(downloadUrl, updateFile); - } - - // Extract Update - updateDialog.MainText.Text = "Extracting Update..."; - updateDialog.ProgressBar.Value = 0; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - using (Stream inStream = File.OpenRead(updateFile)) - using (Stream gzipStream = new GZipInputStream(inStream)) - using (TarInputStream tarStream = new TarInputStream(gzipStream, Encoding.ASCII)) - { - updateDialog.ProgressBar.MaxValue = inStream.Length; - - await Task.Run(() => - { - TarEntry tarEntry; - while ((tarEntry = tarStream.GetNextEntry()) != null) - { - if (tarEntry.IsDirectory) continue; - - string outPath = Path.Combine(UpdateDir, tarEntry.Name); - - Directory.CreateDirectory(Path.GetDirectoryName(outPath)); - - using (FileStream outStream = File.OpenWrite(outPath)) - { - tarStream.CopyEntryContents(outStream); - } - - File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc)); - - TarEntry entry = tarEntry; - - Application.Invoke(delegate - { - updateDialog.ProgressBar.Value += entry.Size; - }); - } - }); - - updateDialog.ProgressBar.Value = inStream.Length; - } - } - else - { - using (Stream inStream = File.OpenRead(updateFile)) - using (ZipFile zipFile = new ZipFile(inStream)) - { - updateDialog.ProgressBar.MaxValue = zipFile.Count; - - await Task.Run(() => - { - foreach (ZipEntry zipEntry in zipFile) - { - if (zipEntry.IsDirectory) continue; - - string outPath = Path.Combine(UpdateDir, zipEntry.Name); - - Directory.CreateDirectory(Path.GetDirectoryName(outPath)); - - using (Stream zipStream = zipFile.GetInputStream(zipEntry)) - using (FileStream outStream = File.OpenWrite(outPath)) - { - zipStream.CopyTo(outStream); - } - - File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc)); - - Application.Invoke(delegate - { - updateDialog.ProgressBar.Value++; - }); - } - }); - } - } - - // Delete downloaded zip - File.Delete(updateFile); - - string[] allFiles = Directory.GetFiles(HomeDir, "*", SearchOption.AllDirectories); - - updateDialog.MainText.Text = "Renaming Old Files..."; - updateDialog.ProgressBar.Value = 0; - updateDialog.ProgressBar.MaxValue = allFiles.Length; - - // Replace old files - await Task.Run(() => - { - foreach (string file in allFiles) - { - if (!Path.GetExtension(file).Equals(".log")) - { - try - { - File.Move(file, file + ".ryuold"); - - Application.Invoke(delegate - { - updateDialog.ProgressBar.Value++; - }); - } - catch - { - Logger.Warning?.Print(LogClass.Application, "Updater wasn't able to rename file: " + file); - } - } - } - - Application.Invoke(delegate - { - updateDialog.MainText.Text = "Adding New Files..."; - updateDialog.ProgressBar.Value = 0; - updateDialog.ProgressBar.MaxValue = Directory.GetFiles(UpdatePublishDir, "*", SearchOption.AllDirectories).Length; - }); - - MoveAllFilesOver(UpdatePublishDir, HomeDir, updateDialog); - }); - - Directory.Delete(UpdateDir, true); - - updateDialog.MainText.Text = "Update Complete!"; - updateDialog.SecondaryText.Text = "Do you want to restart Ryujinx now?"; - updateDialog.Modal = true; - - updateDialog.ProgressBar.Hide(); - updateDialog.YesButton.Show(); - updateDialog.NoButton.Show(); - } - - public static bool CanUpdate(bool showWarnings) - { - if (RuntimeInformation.OSArchitecture != Architecture.X64) - { - if (showWarnings) - { - GtkDialog.CreateWarningDialog("You are not running a supported system architecture!", "(Only x64 systems are supported!)"); - } - - return false; - } - - if (!NetworkInterface.GetIsNetworkAvailable()) - { - if (showWarnings) - { - GtkDialog.CreateWarningDialog("You are not connected to the Internet!", "Please verify that you have a working Internet connection!"); - } - - return false; - } - - if (Program.Version.Contains("dirty")) - { - if (showWarnings) - { - GtkDialog.CreateWarningDialog("You Cannot update a Dirty build of Ryujinx!", "Please download Ryujinx at https://ryujinx.org/ if you are looking for a supported version."); - } - - return false; - } - - return true; - } - - private static void MoveAllFilesOver(string root, string dest, UpdateDialog dialog) - { - foreach (string directory in Directory.GetDirectories(root)) - { - string dirName = Path.GetFileName(directory); - - if (!Directory.Exists(Path.Combine(dest, dirName))) - { - Directory.CreateDirectory(Path.Combine(dest, dirName)); - } - - MoveAllFilesOver(directory, Path.Combine(dest, dirName), dialog); - } - - foreach (string file in Directory.GetFiles(root)) - { - File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true); - - Application.Invoke(delegate - { - dialog.ProgressBar.Value++; - }); - } - } - - public static void CleanupUpdate() - { - foreach (string file in Directory.GetFiles(HomeDir, "*", SearchOption.AllDirectories)) - { - if (Path.GetExtension(file).EndsWith(".ryuold")) - { - File.Delete(file); - } - } - } - } -} -- cgit v1.2.3