From 3ec911a6300a664ae9c5360195b3f65e017e4353 Mon Sep 17 00:00:00 2001 From: Mary Date: Tue, 1 Sep 2020 11:09:42 +0200 Subject: [PATCH] ui: Initial better user error reporting (#1503) This update the "No keys" dialog and block starting NSP/XCI/NCA without firmware. Also propose to the user if they want to install firmware if they start an untrimmed XCI and remove KEYS.md as it was completely outdated. PS: Also fix a bug with "&" in URL with OpenUrl on Windows. --- KEYS.md | 40 ------- Ryujinx/Program.cs | 5 +- Ryujinx/Ui/AboutWindow.cs | 28 +---- Ryujinx/Ui/Diagnostic/GuideDialog.cs | 36 ++++++ Ryujinx/Ui/Diagnostic/SetupValidator.cs | 118 ++++++++++++++++++++ Ryujinx/Ui/Diagnostic/UserError.cs | 39 +++++++ Ryujinx/Ui/Diagnostic/UserErrorDialog.cs | 133 +++++++++++++++++++++++ Ryujinx/Ui/MainWindow.cs | 66 ++++++++++- Ryujinx/Ui/UrlHelper.cs | 29 +++++ 9 files changed, 429 insertions(+), 65 deletions(-) delete mode 100644 KEYS.md create mode 100644 Ryujinx/Ui/Diagnostic/GuideDialog.cs create mode 100644 Ryujinx/Ui/Diagnostic/SetupValidator.cs create mode 100644 Ryujinx/Ui/Diagnostic/UserError.cs create mode 100644 Ryujinx/Ui/Diagnostic/UserErrorDialog.cs create mode 100644 Ryujinx/Ui/UrlHelper.cs diff --git a/KEYS.md b/KEYS.md deleted file mode 100644 index 868e1f06a4..0000000000 --- a/KEYS.md +++ /dev/null @@ -1,40 +0,0 @@ -# Keys - -Keys are required for decrypting most of the file formats used by the Nintendo Switch. - - Keysets are stored as text files. These 2 filenames are automatically read: -* `prod.keys` - Contains common keys used by all Nintendo Switch devices. -* `title.keys` - Contains game-specific keys. - -Ryujinx will first look for keys in `Ryujinx/system`, and if it doesn't find any there it will look in `$HOME/.switch`. -To dump your `prod.keys` and `title.keys` please follow these following steps. -1. First off learn how to boot into RCM mode and inject payloads if you haven't already. This can be done [here](https://nh-server.github.io/switch-guide/). -2. Make sure you have an SD card with the latest release of [Atmosphere](https://github.com/Atmosphere-NX/Atmosphere/releases) inserted into your Nintendo Switch. -3. Download the latest release of [Lockpick_RCM](https://github.com/shchmue/Lockpick_RCM/releases). -4. Boot into RCM mode. -5. Inject the `Lockpick_RCM.bin` that you have downloaded at `Step 3.` using your preferred payload injector. We recommend [TegraRCMGUI](https://github.com/eliboa/TegraRcmGUI/releases) as it is easy to use and has a decent feature set. -6. Using the `Vol+/-` buttons to navigate and the `Power` button to select, select `Dump from SysNAND | Key generation: X` ("X" depends on your Nintendo Switch's firmware version) -7. The dumping process may take a while depending on how many titles you have installed. -8. After its completion press any button to return to the main menu of Lockpick_RCM. -9. Navigate to and select `Power off` if you have an SD card reader. Or you could Navigate and select `Reboot (RCM)` if you want to mount your SD card using `TegraRCMGUI > Tools > Memloader V3 > MMC - SD Card`. -10. You can find your keys in `sd:/switch/prod.keys` and `sd:/switch/title.keys` respectively. -11. Copy these files and paste them in `Ryujinx/system`. -And you're done! - -## Title keys - -These are only used for games that are not dumped from cartridges but from games downloaded from the Nintendo eShop, these are also only used if the eShop dump does *not* have a `ticket`. If the game does have a ticket, Ryujinx will read the key directly from that ticket. - -Title keys are stored in the format `rights_id = key`. - -For example: - -``` -01000000000100000000000000000003 = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -01000000000108000000000000000003 = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -01000000000108000000000000000004 = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -``` - -## Prod keys - -These are typically used to decrypt system files and encrypted game files. These keys get changed in about every major system update, so make sure to keep your keys up-to-date if you want to play newer games! \ No newline at end of file diff --git a/Ryujinx/Program.cs b/Ryujinx/Program.cs index caa8c6f054..f8fb5599e9 100644 --- a/Ryujinx/Program.cs +++ b/Ryujinx/Program.cs @@ -1,11 +1,12 @@ using ARMeilleure.Translation.PTC; using Gtk; +using OpenTK; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Common.SystemInfo; using Ryujinx.Configuration; using Ryujinx.Ui; -using OpenTK; +using Ryujinx.Ui.Diagnostic; using System; using System.IO; using System.Reflection; @@ -110,7 +111,7 @@ namespace Ryujinx bool hasAltProdKeys = !AppDataManager.IsCustomBasePath && File.Exists(Path.Combine(AppDataManager.KeysDirPathAlt, "prod.keys")); if (!hasGlobalProdKeys && !hasAltProdKeys && !Migration.IsMigrationNeeded()) { - GtkDialog.CreateWarningDialog("Key file was not found", "Please refer to `KEYS.md` for more info"); + UserErrorDialog.CreateUserErrorDialog(UserError.NoKeys); } MainWindow mainWindow = new MainWindow(); diff --git a/Ryujinx/Ui/AboutWindow.cs b/Ryujinx/Ui/AboutWindow.cs index 5f1645da5c..50b0bb8a04 100644 --- a/Ryujinx/Ui/AboutWindow.cs +++ b/Ryujinx/Ui/AboutWindow.cs @@ -37,51 +37,35 @@ namespace Ryujinx.Ui _versionText.Text = Program.Version; } - private static void OpenUrl(string url) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Process.Start(new ProcessStartInfo("cmd", $"/c start {url}")); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - Process.Start("xdg-open", url); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - Process.Start("open", url); - } - } - //Events private void RyujinxButton_Pressed(object sender, ButtonPressEventArgs args) { - OpenUrl("https://ryujinx.org"); + UrlHelper.OpenUrl("https://ryujinx.org"); } private void PatreonButton_Pressed(object sender, ButtonPressEventArgs args) { - OpenUrl("https://www.patreon.com/ryujinx"); + UrlHelper.OpenUrl("https://www.patreon.com/ryujinx"); } private void GitHubButton_Pressed(object sender, ButtonPressEventArgs args) { - OpenUrl("https://github.com/Ryujinx/Ryujinx"); + UrlHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx"); } private void DiscordButton_Pressed(object sender, ButtonPressEventArgs args) { - OpenUrl("https://discordapp.com/invite/N2FmfVc"); + UrlHelper.OpenUrl("https://discordapp.com/invite/N2FmfVc"); } private void TwitterButton_Pressed(object sender, ButtonPressEventArgs args) { - OpenUrl("https://twitter.com/RyujinxEmu"); + UrlHelper.OpenUrl("https://twitter.com/RyujinxEmu"); } private void ContributorsButton_Pressed(object sender, ButtonPressEventArgs args) { - OpenUrl("https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a"); + UrlHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a"); } private void CloseToggle_Activated(object sender, EventArgs args) diff --git a/Ryujinx/Ui/Diagnostic/GuideDialog.cs b/Ryujinx/Ui/Diagnostic/GuideDialog.cs new file mode 100644 index 0000000000..c3a0dd38cc --- /dev/null +++ b/Ryujinx/Ui/Diagnostic/GuideDialog.cs @@ -0,0 +1,36 @@ +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 new file mode 100644 index 0000000000..c52dc2ef3e --- /dev/null +++ b/Ryujinx/Ui/Diagnostic/SetupValidator.cs @@ -0,0 +1,118 @@ +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 new file mode 100644 index 0000000000..eaa1bc8320 --- /dev/null +++ b/Ryujinx/Ui/Diagnostic/UserError.cs @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000000..646e98fdcc --- /dev/null +++ b/Ryujinx/Ui/Diagnostic/UserErrorDialog.cs @@ -0,0 +1,133 @@ +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.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.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/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 4c9381ac5d..86a11f072f 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -11,6 +11,7 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.OpenGL; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem.Content; +using Ryujinx.Ui.Diagnostic; using System; using System.Diagnostics; using System.IO; @@ -360,7 +361,70 @@ namespace Ryujinx.Ui UpdateGraphicsConfig(); - Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {_contentManager.GetCurrentFirmwareVersion()?.VersionString}"); + SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion(); + + bool isDirectory = Directory.Exists(path); + + if (!SetupValidator.CanStartApplication(_contentManager, path, out UserError userError)) + { + if (SetupValidator.CanFixStartApplication(_contentManager, path, userError, out firmwareVersion)) + { + 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})" + }; + + if (shouldInstallFirmwareDialog.Run() != (int)ResponseType.Yes) + { + shouldInstallFirmwareDialog.Dispose(); + + UserErrorDialog.CreateUserErrorDialog(userError); + + device.Dispose(); + + return; + } + else + { + shouldInstallFirmwareDialog.Dispose(); + } + } + + if (!SetupValidator.TryFixStartApplication(_contentManager, path, userError, out _)) + { + UserErrorDialog.CreateUserErrorDialog(userError); + + device.Dispose(); + + return; + } + + // Tell the user that we installed a firmware for them. + if (userError == UserError.NoFirmware) + { + firmwareVersion = _contentManager.GetCurrentFirmwareVersion(); + + 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."); + } + } + else + { + UserErrorDialog.CreateUserErrorDialog(userError); + + device.Dispose(); + + return; + } + } + + Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}"); if (Directory.Exists(path)) { diff --git a/Ryujinx/Ui/UrlHelper.cs b/Ryujinx/Ui/UrlHelper.cs new file mode 100644 index 0000000000..79eacc678e --- /dev/null +++ b/Ryujinx/Ui/UrlHelper.cs @@ -0,0 +1,29 @@ +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!"); + } + } + } +}