diff --git a/Ryujinx.HLE/HOS/Applets/AppletManager.cs b/Ryujinx.HLE/HOS/Applets/AppletManager.cs index 5d075882c..a686a8328 100644 --- a/Ryujinx.HLE/HOS/Applets/AppletManager.cs +++ b/Ryujinx.HLE/HOS/Applets/AppletManager.cs @@ -1,4 +1,5 @@ using Ryujinx.HLE.HOS.Applets.Browser; +using Ryujinx.HLE.HOS.Applets.Error; using Ryujinx.HLE.HOS.Services.Am.AppletAE; using System; using System.Collections.Generic; @@ -13,6 +14,7 @@ namespace Ryujinx.HLE.HOS.Applets { _appletMapping = new Dictionary { + { AppletId.Error, typeof(ErrorApplet) }, { AppletId.PlayerSelect, typeof(PlayerSelectApplet) }, { AppletId.Controller, typeof(ControllerApplet) }, { AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) }, diff --git a/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs b/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs new file mode 100644 index 000000000..c78cbf316 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs @@ -0,0 +1,171 @@ +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using Ryujinx.HLE.HOS.SystemState; +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; + +namespace Ryujinx.HLE.HOS.Applets.Error +{ + internal class ErrorApplet : IApplet + { + private const long ErrorMessageBinaryTitleId = 0x0100000000000801; + + private Horizon _horizon; + private AppletSession _normalSession; + private CommonArguments _commonArguments; + private ErrorCommonHeader _errorCommonHeader; + private byte[] _errorStorage; + + public event EventHandler AppletStateChanged; + + public ErrorApplet(Horizon horizon) + { + _horizon = horizon; + } + + public ResultCode Start(AppletSession normalSession, + AppletSession interactiveSession) + { + _normalSession = normalSession; + _commonArguments = IApplet.ReadStruct(_normalSession.Pop()); + + Logger.Info?.PrintMsg(LogClass.ServiceAm, $"ErrorApplet version: 0x{_commonArguments.AppletVersion:x8}"); + + _errorStorage = _normalSession.Pop(); + _errorCommonHeader = IApplet.ReadStruct(_errorStorage); + _errorStorage = _errorStorage.Skip(Marshal.SizeOf(typeof(ErrorCommonHeader))).ToArray(); + + switch (_errorCommonHeader.Type) + { + case ErrorType.ErrorCommonArg: + { + ParseErrorCommonArg(); + + break; + } + default: throw new NotImplementedException($"ErrorApplet type {_errorCommonHeader.Type} is not implemented."); + } + + AppletStateChanged?.Invoke(this, null); + + return ResultCode.Success; + } + + private (uint module, uint description) HexToResultCode(uint resultCode) + { + return ((resultCode & 0x1FF) + 2000, (resultCode >> 9) & 0x3FFF); + } + + private string SystemLanguageToLanguageKey(SystemLanguage systemLanguage) + { + return systemLanguage switch + { + SystemLanguage.Japanese => "ja", + SystemLanguage.AmericanEnglish => "en-US", + SystemLanguage.French => "fr", + SystemLanguage.German => "de", + SystemLanguage.Italian => "it", + SystemLanguage.Spanish => "es", + SystemLanguage.Chinese => "zh-Hans", + SystemLanguage.Korean => "ko", + SystemLanguage.Dutch => "nl", + SystemLanguage.Portuguese => "pt", + SystemLanguage.Russian => "ru", + SystemLanguage.Taiwanese => "zh-HansT", + SystemLanguage.BritishEnglish => "en-GB", + SystemLanguage.CanadianFrench => "fr-CA", + SystemLanguage.LatinAmericanSpanish => "es-419", + SystemLanguage.SimplifiedChinese => "zh-Hans", + SystemLanguage.TraditionalChinese => "zh-Hant", + _ => "en-US" + }; + } + + public string CleanText(string value) + { + return Regex.Replace(Encoding.Unicode.GetString(Encoding.UTF8.GetBytes(value)), @"[^\u0009\u000A\u000D\u0020-\u007E]", ""); + } + + private string GetMessageText(uint module, uint description, string key) + { + string binaryTitleContentPath = _horizon.ContentManager.GetInstalledContentPath(ErrorMessageBinaryTitleId, StorageId.NandSystem, NcaContentType.Data); + + using (LibHac.Fs.IStorage ncaFileStream = new LocalStorage(_horizon.Device.FileSystem.SwitchPathToSystemPath(binaryTitleContentPath), FileAccess.Read, FileMode.Open)) + { + Nca nca = new Nca(_horizon.Device.FileSystem.KeySet, ncaFileStream); + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, _horizon.FsIntegrityCheckLevel); + string languageCode = SystemLanguageToLanguageKey(_horizon.State.DesiredSystemLanguage); + string filePath = "/" + Path.Combine(module.ToString(), $"{description:0000}", $"{languageCode}_{key}").Replace(@"\", "/"); + + if (romfs.FileExists(filePath)) + { + romfs.OpenFile(out IFile binaryFile, filePath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + StreamReader reader = new StreamReader(binaryFile.AsStream()); + + return CleanText(reader.ReadToEnd()); + } + else + { + return ""; + } + } + } + + private string[] GetButtonsText(uint module, uint description, string key) + { + string buttonsText = GetMessageText(module, description, key); + + return (buttonsText == "") ? null : buttonsText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + } + + private void ParseErrorCommonArg() + { + ErrorCommonArg errorCommonArg = IApplet.ReadStruct(_errorStorage); + + uint module = errorCommonArg.Module; + uint description = errorCommonArg.Description; + + if (_errorCommonHeader.MessageFlag == 0) + { + (module, description) = HexToResultCode(errorCommonArg.ResultCode); + } + + string message = GetMessageText(module, description, "DlgMsg"); + + if (message == "") + { + message = "An error has occured.\n\n" + + "Please try again later.\n\n" + + "If the problem persists, please refer to the Ryujinx website.\n" + + "www.ryujinx.org"; + } + + string[] buttons = GetButtonsText(module, description, "DlgBtn"); + + bool showDetails = _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Error Code: {module}-{description:0000}", "\n" + message, buttons); + if (showDetails) + { + message = GetMessageText(module, description, "FlvMsg"); + buttons = GetButtonsText(module, description, "FlvBtn"); + + _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Details: {module}-{description:0000}", "\n" + message, buttons); + } + } + + public ResultCode GetResult() + { + return ResultCode.Success; + } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonArg.cs b/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonArg.cs new file mode 100644 index 000000000..530a2ad8b --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonArg.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.Error +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ErrorCommonArg + { + public uint Module; + public uint Description; + public uint ResultCode; + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonHeader.cs b/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonHeader.cs new file mode 100644 index 000000000..b93cdd4f1 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonHeader.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.Error +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ErrorCommonHeader + { + public ErrorType Type; + public byte JumpFlag; + public byte ReservedFlag1; + public byte ReservedFlag2; + public byte ReservedFlag3; + public byte ContextFlag; + public byte MessageFlag; + public byte ContextFlag2; + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Applets/Error/ErrorType.cs b/Ryujinx.HLE/HOS/Applets/Error/ErrorType.cs new file mode 100644 index 000000000..f06af1d3c --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/Error/ErrorType.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.HLE.HOS.Applets.Error +{ + enum ErrorType : byte + { + ErrorCommonArg, + SystemErrorArg, + ApplicationErrorArg, + ErrorEulaArg, + ErrorPctlArg, + ErrorRecordArg, + SystemUpdateEulaArg = 8 + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/IHostUiHandler.cs b/Ryujinx.HLE/IHostUiHandler.cs index 8e9bfc773..b85fc356a 100644 --- a/Ryujinx.HLE/IHostUiHandler.cs +++ b/Ryujinx.HLE/IHostUiHandler.cs @@ -31,5 +31,10 @@ namespace Ryujinx.HLE /// The program kind. /// The value associated to the . void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value); + + /// Displays a Message Dialog box specific to Error Applet and blocks until it is closed. + /// + /// False when OK is pressed, True when another button (Details) is pressed. + bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText); } } \ No newline at end of file diff --git a/Ryujinx/Ui/ErrorAppletDialog.cs b/Ryujinx/Ui/ErrorAppletDialog.cs new file mode 100644 index 000000000..f7a548b3e --- /dev/null +++ b/Ryujinx/Ui/ErrorAppletDialog.cs @@ -0,0 +1,32 @@ +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/GtkHostUiHandler.cs b/Ryujinx/Ui/GtkHostUiHandler.cs index fd193dd7c..12ba81c44 100644 --- a/Ryujinx/Ui/GtkHostUiHandler.cs +++ b/Ryujinx/Ui/GtkHostUiHandler.cs @@ -128,5 +128,56 @@ namespace Ryujinx.Ui 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; + } } }