From f16d7f91f1e0483a55c23382171bb81a679e4d8c Mon Sep 17 00:00:00 2001 From: Caian Benedicto Date: Wed, 10 Feb 2021 21:28:44 -0300 Subject: [PATCH] Improve inline keyboard compatibility (#1959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve compatibility of the inline keyboard with some games * Send an empty first text to avoid crashing some games * Implement SetCustomizedDictionaries and fix SetCustomizeDic * Expand Bg and Fg –abbreviations in the swkbd applet * Fix variable names and add comments to software keyboard --- .../SoftwareKeyboard/InlineKeyboardRequest.cs | 48 ++- .../InlineKeyboardResponse.cs | 101 ++++- .../SoftwareKeyboard/InlineKeyboardState.cs | 27 +- .../SoftwareKeyboard/InlineResponses.cs | 70 +-- .../SoftwareKeyboardAppear.cs | 34 +- .../SoftwareKeyboardApplet.cs | 406 ++++++++++++------ .../SoftwareKeyboard/SoftwareKeyboardCalc.cs | 79 +++- .../SoftwareKeyboardCustomizeDic.cs | 14 + .../SoftwareKeyboardDictSet.cs | 29 +- .../SoftwareKeyboardInitialize.cs | 11 +- .../SoftwareKeyboardUserWord.cs | 14 + 11 files changed, 626 insertions(+), 207 deletions(-) create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs index d0024001..b17debfc 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs @@ -1,18 +1,48 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// - /// Possible requests to the keyboard when running in inline mode. + /// Possible requests to the software keyboard when running in inline mode. /// enum InlineKeyboardRequest : uint { - Unknown0 = 0x0, - Finalize = 0x4, - SetUserWordInfo = 0x6, - SetCustomizeDic = 0x7, - Calc = 0xA, - SetCustomizedDictionaries = 0xB, + /// + /// Finalize the keyboard applet. + /// + Finalize = 0x4, + + /// + /// Set user words for text prediction. + /// + SetUserWordInfo = 0x6, + + /// + /// Sets the CustomizeDic data. Can't be used if CustomizedDictionaries is already set. + /// + SetCustomizeDic = 0x7, + + /// + /// Configure the keyboard applet and put it in a state where it is processing input. + /// + Calc = 0xA, + + /// + /// Set custom dictionaries for text prediction. Can't be used if SetCustomizeDic is already set. + /// + SetCustomizedDictionaries = 0xB, + + /// + /// Release custom dictionaries data. + /// UnsetCustomizedDictionaries = 0xC, - UseChangedStringV2 = 0xD, - UseMovedCursorV2 = 0xE + + /// + /// [8.0.0+] Request the keyboard applet to use the ChangedStringV2 response when notifying changes in text data. + /// + UseChangedStringV2 = 0xD, + + /// + /// [8.0.0+] Request the keyboard applet to use the MovedCursorV2 response when notifying changes in cursor position. + /// + UseMovedCursorV2 = 0xE } } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs index 61908b7b..b21db507 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs @@ -1,26 +1,93 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// - /// Possible responses from the keyboard when running in inline mode. + /// Possible responses from the software keyboard when running in inline mode. /// enum InlineKeyboardResponse : uint { - FinishedInitialize = 0x0, - Default = 0x1, - ChangedString = 0x2, - MovedCursor = 0x3, - MovedTab = 0x4, - DecidedEnter = 0x5, - DecidedCancel = 0x6, - ChangedStringUtf8 = 0x7, - MovedCursorUtf8 = 0x8, - DecidedEnterUtf8 = 0x9, - UnsetCustomizeDic = 0xA, - ReleasedUserWordInfo = 0xB, + /// + /// The software keyboard received a Calc and it is fully initialized. Reply data is ignored by the user-process. + /// + FinishedInitialize = 0x0, + + /// + /// Default response. Official sw has no handling for this besides just closing the storage. + /// + Default = 0x1, + + /// + /// The text data in the software keyboard changed (UTF-16 encoding). + /// + ChangedString = 0x2, + + /// + /// The cursor position in the software keyboard changed (UTF-16 encoding). + /// + MovedCursor = 0x3, + + /// + /// A tab in the software keyboard changed. + /// + MovedTab = 0x4, + + /// + /// The OK key was pressed in the software keyboard, confirming the input text (UTF-16 encoding). + /// + DecidedEnter = 0x5, + + /// + /// The Cancel key was pressed in the software keyboard, cancelling the input. + /// + DecidedCancel = 0x6, + + /// + /// Same as ChangedString, but with UTF-8 encoding. + /// + ChangedStringUtf8 = 0x7, + + /// + /// Same as MovedCursor, but with UTF-8 encoding. + /// + MovedCursorUtf8 = 0x8, + + /// + /// Same as DecidedEnter, but with UTF-8 encoding. + /// + DecidedEnterUtf8 = 0x9, + + /// + /// They software keyboard is releasing the data previously set by a SetCustomizeDic request. + /// + UnsetCustomizeDic = 0xA, + + /// + /// They software keyboard is releasing the data previously set by a SetUserWordInfo request. + /// + ReleasedUserWordInfo = 0xB, + + /// + /// They software keyboard is releasing the data previously set by a SetCustomizedDictionaries request. + /// UnsetCustomizedDictionaries = 0xC, - ChangedStringV2 = 0xD, - MovedCursorV2 = 0xE, - ChangedStringUtf8V2 = 0xF, - MovedCursorUtf8V2 = 0x10 + + /// + /// Same as ChangedString, but with additional fields. + /// + ChangedStringV2 = 0xD, + + /// + /// Same as MovedCursor, but with additional fields. + /// + MovedCursorV2 = 0xE, + + /// + /// Same as ChangedStringUtf8, but with additional fields. + /// + ChangedStringUtf8V2 = 0xF, + + /// + /// Same as MovedCursorUtf8, but with additional fields. + /// + MovedCursorUtf8V2 = 0x10 } } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs index 2940d161..024ff2cf 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs @@ -1,14 +1,33 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// - /// Possible states for the keyboard when running in inline mode. + /// Possible states for the software keyboard when running in inline mode. /// enum InlineKeyboardState : uint { + /// + /// The software keyboard has just been created or finalized and is uninitialized. + /// Uninitialized = 0x0, - Initializing = 0x1, - Ready = 0x2, + + /// + /// A Calc was previously received and fulfilled, so the software keyboard is initialized, but is not processing input. + /// + Initialized = 0x1, + + /// + /// A Calc was received and the software keyboard is processing input. + /// + Ready = 0x2, + + /// + /// New text data or cursor position of the software keyboard are available. + /// DataAvailable = 0x3, - Completed = 0x4 + + /// + /// The Calc request was fulfilled with either a text input or a cancel. + /// + Complete = 0x4 } } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs index 60cc5287..50e77b74 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs @@ -45,41 +45,41 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard writer.Write(cursor); // Cursor position } - public static byte[] FinishedInitialize() + public static byte[] FinishedInitialize(InlineKeyboardState state) { uint resSize = 2 * sizeof(uint) + 0x1; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Ready, InlineKeyboardResponse.FinishedInitialize, writer); + BeginResponse(state, InlineKeyboardResponse.FinishedInitialize, writer); writer.Write((byte)1); // Data (ignored by the program) return stream.ToArray(); } } - public static byte[] Default() + public static byte[] Default(InlineKeyboardState state) { uint resSize = 2 * sizeof(uint); using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.Default, writer); + BeginResponse(state, InlineKeyboardResponse.Default, writer); return stream.ToArray(); } } - public static byte[] ChangedString(string text) + public static byte[] ChangedString(string text, InlineKeyboardState state) { - uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16; + uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.ChangedString, writer); + BeginResponse(state, InlineKeyboardResponse.ChangedString, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); writer.Write((int)0); // ? writer.Write((int)0); // ? @@ -88,21 +88,21 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard } } - public static byte[] MovedCursor(string text) + public static byte[] MovedCursor(string text, InlineKeyboardState state) { uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.MovedCursor, writer); + BeginResponse(state, InlineKeyboardResponse.MovedCursor, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); return stream.ToArray(); } } - public static byte[] MovedTab(string text) + public static byte[] MovedTab(string text, InlineKeyboardState state) { // Should be the same as MovedCursor. @@ -111,48 +111,48 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.MovedTab, writer); + BeginResponse(state, InlineKeyboardResponse.MovedTab, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); return stream.ToArray(); } } - public static byte[] DecidedEnter(string text) + public static byte[] DecidedEnter(string text, InlineKeyboardState state) { uint resSize = 3 * sizeof(uint) + MaxStrLenUTF16; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Completed, InlineKeyboardResponse.DecidedEnter, writer); + BeginResponse(state, InlineKeyboardResponse.DecidedEnter, writer); WriteString(text, writer, MaxStrLenUTF16, Encoding.Unicode); return stream.ToArray(); } } - public static byte[] DecidedCancel() + public static byte[] DecidedCancel(InlineKeyboardState state) { uint resSize = 2 * sizeof(uint); using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Completed, InlineKeyboardResponse.DecidedCancel, writer); + BeginResponse(state, InlineKeyboardResponse.DecidedCancel, writer); return stream.ToArray(); } } - public static byte[] ChangedStringUtf8(string text) + public static byte[] ChangedStringUtf8(string text, InlineKeyboardState state) { uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.ChangedStringUtf8, writer); + BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); writer.Write((int)0); // ? writer.Write((int)0); // ? @@ -161,81 +161,81 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard } } - public static byte[] MovedCursorUtf8(string text) + public static byte[] MovedCursorUtf8(string text, InlineKeyboardState state) { uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.MovedCursorUtf8, writer); + BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); return stream.ToArray(); } } - public static byte[] DecidedEnterUtf8(string text) + public static byte[] DecidedEnterUtf8(string text, InlineKeyboardState state) { uint resSize = 3 * sizeof(uint) + MaxStrLenUTF8; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Completed, InlineKeyboardResponse.DecidedEnterUtf8, writer); + BeginResponse(state, InlineKeyboardResponse.DecidedEnterUtf8, writer); WriteString(text, writer, MaxStrLenUTF8, Encoding.UTF8); return stream.ToArray(); } } - public static byte[] UnsetCustomizeDic() + public static byte[] UnsetCustomizeDic(InlineKeyboardState state) { uint resSize = 2 * sizeof(uint); using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.UnsetCustomizeDic, writer); + BeginResponse(state, InlineKeyboardResponse.UnsetCustomizeDic, writer); return stream.ToArray(); } } - public static byte[] ReleasedUserWordInfo() + public static byte[] ReleasedUserWordInfo(InlineKeyboardState state) { uint resSize = 2 * sizeof(uint); using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.ReleasedUserWordInfo, writer); + BeginResponse(state, InlineKeyboardResponse.ReleasedUserWordInfo, writer); return stream.ToArray(); } } - public static byte[] UnsetCustomizedDictionaries() + public static byte[] UnsetCustomizedDictionaries(InlineKeyboardState state) { uint resSize = 2 * sizeof(uint); using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.UnsetCustomizedDictionaries, writer); + BeginResponse(state, InlineKeyboardResponse.UnsetCustomizedDictionaries, writer); return stream.ToArray(); } } - public static byte[] ChangedStringV2(string text) + public static byte[] ChangedStringV2(string text, InlineKeyboardState state) { uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16 + 0x1; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.ChangedStringV2, writer); + BeginResponse(state, InlineKeyboardResponse.ChangedStringV2, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); writer.Write((int)0); // ? writer.Write((int)0); // ? @@ -245,14 +245,14 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard } } - public static byte[] MovedCursorV2(string text) + public static byte[] MovedCursorV2(string text, InlineKeyboardState state) { uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16 + 0x1; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.MovedCursorV2, writer); + BeginResponse(state, InlineKeyboardResponse.MovedCursorV2, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); writer.Write((byte)0); // Flag == 0 @@ -260,14 +260,14 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard } } - public static byte[] ChangedStringUtf8V2(string text) + public static byte[] ChangedStringUtf8V2(string text, InlineKeyboardState state) { uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8 + 0x1; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.ChangedStringUtf8V2, writer); + BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8V2, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); writer.Write((int)0); // ? writer.Write((int)0); // ? @@ -277,14 +277,14 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard } } - public static byte[] MovedCursorUtf8V2(string text) + public static byte[] MovedCursorUtf8V2(string text, InlineKeyboardState state) { uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8 + 0x1; using (MemoryStream stream = new MemoryStream(new byte[resSize])) using (BinaryWriter writer = new BinaryWriter(stream)) { - BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.MovedCursorUtf8V2, writer); + BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8V2, writer); WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); writer.Write((byte)0); // Flag == 0 diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs index 23e8bd1f..262dd4df 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs @@ -2,33 +2,38 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { + /// + /// A structure with appearance configurations for the software keyboard when running in inline mode. + /// [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct SoftwareKeyboardAppear { private const int OkTextLength = 8; - // Some games send a Calc without intention of showing the keyboard, a - // common trend observed is that this field will be != 0 in such cases. + /// + /// Some games send a Calc without intention of showing the keyboard, a + /// common trend observed is that this field will be != 0 in such cases. + /// public uint ShouldBeHidden; + /// + /// The string displayed in the Submit button. + /// [MarshalAs(UnmanagedType.ByValTStr, SizeConst = OkTextLength + 1)] public string OkText; /// /// The character displayed in the left button of the numeric keyboard. - /// This is ignored when Mode is not set to NumbersOnly. /// public char LeftOptionalSymbolKey; /// /// The character displayed in the right button of the numeric keyboard. - /// This is ignored when Mode is not set to NumbersOnly. /// public char RightOptionalSymbolKey; /// - /// When set, predictive typing is enabled making use of the system dictionary, - /// and any custom user dictionary. + /// When set, predictive typing is enabled making use of the system dictionary, and any custom user dictionary. /// [MarshalAs(UnmanagedType.I1)] public bool PredictionEnabled; @@ -43,13 +48,24 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard public int Padding1; public int Padding2; - public byte EnableReturnButton; + /// + /// Indicates the return button is enabled in the keyboard. This allows for input with multiple lines. + /// + [MarshalAs(UnmanagedType.I1)] + public bool UseNewLine; + + /// + /// [10.0.0+] If value is 1 or 2, then keytopAsFloating=0 and footerScalable=1 in Calc. + /// + public byte Unknown1; - public byte Padding3; public byte Padding4; public byte Padding5; - public uint CalcArgFlags; + /// + /// Bitmask 0x1000 of the Calc and DirectionalButtonAssignEnabled in bitmask 0x10000000. + /// + public uint CalcFlags; public uint Padding6; public uint Padding7; diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs index 89ed5592..4f43472d 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs @@ -1,4 +1,5 @@ -using Ryujinx.Common.Logging; +using Ryujinx.Common; +using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; using Ryujinx.HLE.HOS.Services.Am.AppletAE; using System; @@ -14,31 +15,41 @@ namespace Ryujinx.HLE.HOS.Applets { private const string DefaultText = "Ryujinx"; + private const long DebounceTimeMillis = 200; + private const int ResetDelayMillis = 500; + private readonly Switch _device; private const int StandardBufferSize = 0x7D8; private const int InteractiveBufferSize = 0x7D4; + private const int MaxUserWords = 0x1388; - private SoftwareKeyboardState _state = SoftwareKeyboardState.Uninitialized; + private SoftwareKeyboardState _foregroundState = SoftwareKeyboardState.Uninitialized; + private volatile InlineKeyboardState _backgroundState = InlineKeyboardState.Uninitialized; private bool _isBackground = false; + private bool _alreadyShown = false; + private volatile bool _useChangedStringV2 = false; private AppletSession _normalSession; private AppletSession _interactiveSession; - // Configuration for foreground mode - private SoftwareKeyboardConfig _keyboardFgConfig; - private SoftwareKeyboardCalc _keyboardCalc; - private SoftwareKeyboardDictSet _keyboardDict; + // Configuration for foreground mode. + private SoftwareKeyboardConfig _keyboardForegroundConfig; - // Configuration for background mode - private SoftwareKeyboardInitialize _keyboardBgInitialize; + // Configuration for background (inline) mode. + private SoftwareKeyboardInitialize _keyboardBackgroundInitialize; + private SoftwareKeyboardCalc _keyboardBackgroundCalc; + private SoftwareKeyboardCustomizeDic _keyboardBackgroundDic; + private SoftwareKeyboardDictSet _keyboardBackgroundDictSet; + private SoftwareKeyboardUserWord[] _keyboardBackgroundUserWords; private byte[] _transferMemory; - private string _textValue = null; + private string _textValue = ""; private bool _okPressed = false; private Encoding _encoding = Encoding.Unicode; + private long _lastTextSetMillis = 0; public event EventHandler AppletStateChanged; @@ -55,22 +66,27 @@ namespace Ryujinx.HLE.HOS.Applets _interactiveSession.DataAvailable += OnInteractiveData; + _alreadyShown = false; + _useChangedStringV2 = false; + var launchParams = _normalSession.Pop(); var keyboardConfig = _normalSession.Pop(); - // TODO: A better way would be handling the background creation properly - // in LibraryAppleCreator / Acessor instead of guessing by size. if (keyboardConfig.Length == Marshal.SizeOf()) { + // Initialize the keyboard applet in background mode. + _isBackground = true; - _keyboardBgInitialize = ReadStruct(keyboardConfig); - _state = SoftwareKeyboardState.Uninitialized; + _keyboardBackgroundInitialize = ReadStruct(keyboardConfig); + _backgroundState = InlineKeyboardState.Uninitialized; return ResultCode.Success; } else { + // Initialize the keyboard applet in foreground mode. + _isBackground = false; if (keyboardConfig.Length < Marshal.SizeOf()) @@ -79,7 +95,7 @@ namespace Ryujinx.HLE.HOS.Applets } else { - _keyboardFgConfig = ReadStruct(keyboardConfig); + _keyboardForegroundConfig = ReadStruct(keyboardConfig); } if (!_normalSession.TryPop(out _transferMemory)) @@ -87,12 +103,12 @@ namespace Ryujinx.HLE.HOS.Applets Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null"); } - if (_keyboardFgConfig.UseUtf8) + if (_keyboardForegroundConfig.UseUtf8) { _encoding = Encoding.UTF8; } - _state = SoftwareKeyboardState.Ready; + _foregroundState = SoftwareKeyboardState.Ready; ExecuteForegroundKeyboard(); @@ -105,32 +121,44 @@ namespace Ryujinx.HLE.HOS.Applets return ResultCode.Success; } + private InlineKeyboardState GetInlineState() + { + return _backgroundState; + } + + private void SetInlineState(InlineKeyboardState state) + { + _backgroundState = state; + } + private void ExecuteForegroundKeyboard() { string initialText = null; // Initial Text is always encoded as a UTF-16 string in the work buffer (passed as transfer memory) // InitialStringOffset points to the memory offset and InitialStringLength is the number of UTF-16 characters - if (_transferMemory != null && _keyboardFgConfig.InitialStringLength > 0) + if (_transferMemory != null && _keyboardForegroundConfig.InitialStringLength > 0) { - initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardFgConfig.InitialStringOffset, 2 * _keyboardFgConfig.InitialStringLength); + initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardForegroundConfig.InitialStringOffset, + 2 * _keyboardForegroundConfig.InitialStringLength); } // If the max string length is 0, we set it to a large default // length. - if (_keyboardFgConfig.StringLengthMax == 0) + if (_keyboardForegroundConfig.StringLengthMax == 0) { - _keyboardFgConfig.StringLengthMax = 100; + _keyboardForegroundConfig.StringLengthMax = 100; } var args = new SoftwareKeyboardUiArgs { - HeaderText = _keyboardFgConfig.HeaderText, - SubtitleText = _keyboardFgConfig.SubtitleText, - GuideText = _keyboardFgConfig.GuideText, - SubmitText = (!string.IsNullOrWhiteSpace(_keyboardFgConfig.SubmitText) ? _keyboardFgConfig.SubmitText : "OK"), - StringLengthMin = _keyboardFgConfig.StringLengthMin, - StringLengthMax = _keyboardFgConfig.StringLengthMax, + HeaderText = _keyboardForegroundConfig.HeaderText, + SubtitleText = _keyboardForegroundConfig.SubtitleText, + GuideText = _keyboardForegroundConfig.GuideText, + SubmitText = (!string.IsNullOrWhiteSpace(_keyboardForegroundConfig.SubmitText) ? + _keyboardForegroundConfig.SubmitText : "OK"), + StringLengthMin = _keyboardForegroundConfig.StringLengthMin, + StringLengthMax = _keyboardForegroundConfig.StringLengthMax, InitialText = initialText }; @@ -151,26 +179,26 @@ namespace Ryujinx.HLE.HOS.Applets // than our default text, repeat our default text until we meet // the minimum length requirement. // This should always be done before the text truncation step. - while (_textValue.Length < _keyboardFgConfig.StringLengthMin) + while (_textValue.Length < _keyboardForegroundConfig.StringLengthMin) { _textValue = String.Join(" ", _textValue, _textValue); } // If our default text is longer than the allowed length, // we truncate it. - if (_textValue.Length > _keyboardFgConfig.StringLengthMax) + if (_textValue.Length > _keyboardForegroundConfig.StringLengthMax) { - _textValue = _textValue.Substring(0, (int)_keyboardFgConfig.StringLengthMax); + _textValue = _textValue.Substring(0, (int)_keyboardForegroundConfig.StringLengthMax); } // Does the application want to validate the text itself? - if (_keyboardFgConfig.CheckText) + if (_keyboardForegroundConfig.CheckText) { // The application needs to validate the response, so we // submit it to the interactive output buffer, and poll it // for validation. Once validated, the application will submit // back a validation status, which is handled in OnInteractiveDataPushIn. - _state = SoftwareKeyboardState.ValidationPending; + _foregroundState = SoftwareKeyboardState.ValidationPending; _interactiveSession.Push(BuildResponse(_textValue, true)); } @@ -179,7 +207,7 @@ namespace Ryujinx.HLE.HOS.Applets // If the application doesn't need to validate the response, // we push the data to the non-interactive output buffer // and poll it for completion. - _state = SoftwareKeyboardState.Complete; + _foregroundState = SoftwareKeyboardState.Complete; _normalSession.Push(BuildResponse(_textValue, false)); @@ -204,7 +232,7 @@ namespace Ryujinx.HLE.HOS.Applets private void OnForegroundInteractiveData(byte[] data) { - if (_state == SoftwareKeyboardState.ValidationPending) + if (_foregroundState == SoftwareKeyboardState.ValidationPending) { // TODO(jduncantor): // If application rejects our "attempt", submit another attempt, @@ -216,9 +244,9 @@ namespace Ryujinx.HLE.HOS.Applets AppletStateChanged?.Invoke(this, null); - _state = SoftwareKeyboardState.Complete; + _foregroundState = SoftwareKeyboardState.Complete; } - else if(_state == SoftwareKeyboardState.Complete) + else if(_foregroundState == SoftwareKeyboardState.Complete) { // If we have already completed, we push the result text // back on the output buffer and poll the application. @@ -242,142 +270,278 @@ namespace Ryujinx.HLE.HOS.Applets using (MemoryStream stream = new MemoryStream(data)) using (BinaryReader reader = new BinaryReader(stream)) { - var request = (InlineKeyboardRequest)reader.ReadUInt32(); - + InlineKeyboardRequest request = (InlineKeyboardRequest)reader.ReadUInt32(); + InlineKeyboardState state = GetInlineState(); long remaining; - // Always show the keyboard if the state is 'Ready'. - bool showKeyboard = _state == SoftwareKeyboardState.Ready; + Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {state}"); switch (request) { - case InlineKeyboardRequest.Unknown0: // Unknown request sent by some games after calc - _interactiveSession.Push(InlineResponses.Default()); - break; case InlineKeyboardRequest.UseChangedStringV2: - // Not used because we only send the entire string after confirmation. - _interactiveSession.Push(InlineResponses.Default()); + _useChangedStringV2 = true; break; case InlineKeyboardRequest.UseMovedCursorV2: - // Not used because we only send the entire string after confirmation. - _interactiveSession.Push(InlineResponses.Default()); + // Not used because we only reply with the final string. + break; + case InlineKeyboardRequest.SetUserWordInfo: + // Read the user word info data. + remaining = stream.Length - stream.Position; + if (remaining < sizeof(int)) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes"); + } + else + { + int wordsCount = reader.ReadInt32(); + int wordSize = Marshal.SizeOf(); + remaining = stream.Length - stream.Position; + + if (wordsCount > MaxUserWords) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}"); + } + else if (wordsCount * wordSize != remaining) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words"); + } + else + { + _keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount]; + + for (int word = 0; word < wordsCount; word++) + { + byte[] wordData = reader.ReadBytes(wordSize); + _keyboardBackgroundUserWords[word] = ReadStruct(wordData); + } + } + } + _interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(state)); break; case InlineKeyboardRequest.SetCustomizeDic: + // Read the custom dic data. + remaining = stream.Length - stream.Position; + if (remaining != Marshal.SizeOf()) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes"); + } + else + { + var keyboardDicData = reader.ReadBytes((int)remaining); + _keyboardBackgroundDic = ReadStruct(keyboardDicData); + } + _interactiveSession.Push(InlineResponses.UnsetCustomizeDic(state)); + break; + case InlineKeyboardRequest.SetCustomizedDictionaries: + // Read the custom dictionaries data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf()) { - Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes!"); + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes"); } else { var keyboardDictData = reader.ReadBytes((int)remaining); - _keyboardDict = ReadStruct(keyboardDictData); + _keyboardBackgroundDictSet = ReadStruct(keyboardDictData); } - _interactiveSession.Push(InlineResponses.Default()); + _interactiveSession.Push(InlineResponses.UnsetCustomizedDictionaries(state)); break; case InlineKeyboardRequest.Calc: - // Put the keyboard in a Ready state, this will force showing - _state = SoftwareKeyboardState.Ready; + // The Calc request tells the Applet to enter the main input handling loop, which will end + // with either a text being submitted or a cancel request from the user. + + // NOTE: Some Calc requests happen early in the process and are not meant to be shown. This possibly + // happens because the game has complete control over when the inline keyboard is drawn, but here it + // would cause a dialog to pop in the emulator, which is inconvenient. An algorithm is applied to + // decide whether it is a dummy Calc or not, but regardless of the result, the dummy Calc appears to + // never happen twice, so the keyboard will always show if it has already been shown before. + bool forceShowKeyboard = _alreadyShown; + _alreadyShown = true; + + // Read the Calc data. remaining = stream.Length - stream.Position; if (remaining != Marshal.SizeOf()) { - Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes!"); + Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes"); } else { var keyboardCalcData = reader.ReadBytes((int)remaining); - _keyboardCalc = ReadStruct(keyboardCalcData); + _keyboardBackgroundCalc = ReadStruct(keyboardCalcData); - if (_keyboardCalc.Utf8Mode == 0x1) + // Check if the application expects UTF8 encoding instead of UTF16. + if (_keyboardBackgroundCalc.UseUtf8) { _encoding = Encoding.UTF8; } // Force showing the keyboard regardless of the state, an unwanted // input dialog may show, but it is better than a soft lock. - if (_keyboardCalc.Appear.ShouldBeHidden == 0) + if (_keyboardBackgroundCalc.Appear.ShouldBeHidden == 0) { - showKeyboard = true; + forceShowKeyboard = true; } } // Send an initialization finished signal. - _interactiveSession.Push(InlineResponses.FinishedInitialize()); + state = InlineKeyboardState.Ready; + SetInlineState(state); + _interactiveSession.Push(InlineResponses.FinishedInitialize(state)); // Start a task with the GUI handler to get user's input. - new Task(() => - { - bool submit = true; - string inputText = (!string.IsNullOrWhiteSpace(_keyboardCalc.InputText) ? _keyboardCalc.InputText : DefaultText); - - // Call the configured GUI handler to get user's input. - if (!showKeyboard) - { - // Submit the default text to avoid soft locking if the keyboard was ignored by - // accident. It's better to change the name than being locked out of the game. - submit = true; - inputText = DefaultText; - - Logger.Debug?.Print(LogClass.Application, "Received a dummy Calc, keyboard will not be shown"); - } - else if (_device.UiHandler == null) - { - Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default"); - } - else - { - var args = new SoftwareKeyboardUiArgs - { - HeaderText = "", // The inline keyboard lacks these texts - SubtitleText = "", - GuideText = "", - SubmitText = (!string.IsNullOrWhiteSpace(_keyboardCalc.Appear.OkText) ? _keyboardCalc.Appear.OkText : "OK"), - StringLengthMin = 0, - StringLengthMax = 100, - InitialText = inputText - }; - - submit = _device.UiHandler.DisplayInputDialog(args, out inputText); - } - - if (submit) - { - Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard OK..."); - - if (_encoding == Encoding.UTF8) - { - _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(inputText)); - } - else - { - _interactiveSession.Push(InlineResponses.DecidedEnter(inputText)); - } - } - else - { - Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel..."); - _interactiveSession.Push(InlineResponses.DecidedCancel()); - } - - // TODO: Why is this necessary? Does the software expect a constant stream of responses? - Thread.Sleep(500); - - Logger.Debug?.Print(LogClass.ServiceAm, "Resetting state of the keyboard..."); - _interactiveSession.Push(InlineResponses.Default()); - }).Start(); + new Task(() => { GetInputTextAndSend(forceShowKeyboard, state); }).Start(); break; case InlineKeyboardRequest.Finalize: - // The game wants to close the keyboard applet and will wait for a state change. - _state = SoftwareKeyboardState.Uninitialized; + // The calling process wants to close the keyboard applet and will wait for a state change. + _backgroundState = InlineKeyboardState.Uninitialized; AppletStateChanged?.Invoke(this, null); break; default: // We shouldn't be able to get here through standard swkbd execution. - Logger.Error?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_state}!"); - _interactiveSession.Push(InlineResponses.Default()); + Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_backgroundState}"); + _interactiveSession.Push(InlineResponses.Default(state)); break; } } } + private void GetInputTextAndSend(bool forceShowKeyboard, InlineKeyboardState oldState) + { + bool submit = true; + + // Use the text specified by the Calc if it is available, otherwise use the default one. + string inputText = (!string.IsNullOrWhiteSpace(_keyboardBackgroundCalc.InputText) ? + _keyboardBackgroundCalc.InputText : DefaultText); + + // Compute the elapsed time for the debouncing algorithm. + long currentMillis = PerformanceCounter.ElapsedMilliseconds; + long inputElapsedMillis = currentMillis - _lastTextSetMillis; + + // Reset the input text before submitting the final result, that's because some games do not expect + // consecutive submissions to abruptly shrink and they will crash if it happens. Changing the string + // before the final submission prevents that. + InlineKeyboardState newState = InlineKeyboardState.DataAvailable; + SetInlineState(newState); + ChangedString("", newState); + + if (inputElapsedMillis < DebounceTimeMillis) + { + // A repeated Calc request has been received without player interaction, after the input has been + // sent. This behavior happens in some games, so instead of showing another dialog, just apply a + // time-based debouncing algorithm and repeat the last submission, either a value or a cancel. + inputText = _textValue; + submit = _textValue != null; + + Logger.Warning?.Print(LogClass.Application, "Debouncing repeated keyboard request"); + } + else if (!forceShowKeyboard) + { + // Submit the default text to avoid soft locking if the keyboard was ignored by + // accident. It's better to change the name than being locked out of the game. + inputText = DefaultText; + + Logger.Debug?.Print(LogClass.Application, "Received a dummy Calc, keyboard will not be shown"); + } + else if (_device.UiHandler == null) + { + Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default"); + } + else + { + // Call the configured GUI handler to get user's input. + var args = new SoftwareKeyboardUiArgs + { + HeaderText = "", // The inline keyboard lacks these texts + SubtitleText = "", + GuideText = "", + SubmitText = (!string.IsNullOrWhiteSpace(_keyboardBackgroundCalc.Appear.OkText) ? + _keyboardBackgroundCalc.Appear.OkText : "OK"), + StringLengthMin = 0, + StringLengthMax = 100, + InitialText = inputText + }; + + submit = _device.UiHandler.DisplayInputDialog(args, out inputText); + inputText = submit ? inputText : null; + } + + // The 'Complete' state indicates the Calc request has been fulfilled by the applet. + newState = InlineKeyboardState.Complete; + + if (submit) + { + Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard OK"); + DecidedEnter(inputText, newState); + } + else + { + Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel"); + DecidedCancel(newState); + } + + _interactiveSession.Push(InlineResponses.Default(newState)); + + // The constant calls to PopInteractiveData suggest that the keyboard applet continuously reports + // data back to the application and this can also be time-sensitive. Pushing a state reset right + // after the data has been sent does not work properly and the application will soft-lock. This + // delay gives time for the application to catch up with the data and properly process the state + // reset. + Thread.Sleep(ResetDelayMillis); + + // 'Initialized' is the only known state so far that does not soft-lock the keyboard after use. + newState = InlineKeyboardState.Initialized; + + Logger.Debug?.Print(LogClass.ServiceAm, $"Resetting state of the keyboard to {newState}"); + + SetInlineState(newState); + _interactiveSession.Push(InlineResponses.Default(newState)); + + // Keep the text and the timestamp of the input for the debouncing algorithm. + _textValue = inputText; + _lastTextSetMillis = PerformanceCounter.ElapsedMilliseconds; + } + + private void ChangedString(string text, InlineKeyboardState state) + { + if (_encoding == Encoding.UTF8) + { + if (_useChangedStringV2) + { + _interactiveSession.Push(InlineResponses.ChangedStringUtf8V2(text, state)); + } + else + { + _interactiveSession.Push(InlineResponses.ChangedStringUtf8(text, state)); + } + } + else + { + if (_useChangedStringV2) + { + _interactiveSession.Push(InlineResponses.ChangedStringV2(text, state)); + } + else + { + _interactiveSession.Push(InlineResponses.ChangedString(text, state)); + } + } + } + + private void DecidedEnter(string text, InlineKeyboardState state) + { + if (_encoding == Encoding.UTF8) + { + _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(text, state)); + } + else + { + _interactiveSession.Push(InlineResponses.DecidedEnter(text, state)); + } + } + + private void DecidedCancel(InlineKeyboardState state) + { + _interactiveSession.Push(InlineResponses.DecidedCancel(state)); + } + private byte[] BuildResponse(string text, bool interactive) { int bufferSize = interactive ? InteractiveBufferSize : StandardBufferSize; diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs index 6213c2e4..a80690c3 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs @@ -3,7 +3,7 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// - /// A structure that defines the configuration options of the software keyboard. + /// A structure with configuration options of the software keyboard when starting a new input request in inline mode. /// [StructLayout(LayoutKind.Sequential, Pack=1, CharSet = CharSet.Unicode)] struct SoftwareKeyboardCalc @@ -12,28 +12,56 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard public uint Unknown; + /// + /// The size of the Calc struct, as reported by the process communicating with the applet. + /// public ushort Size; public byte Unknown1; public byte Unknown2; + /// + /// Configuration flags. Their purpose is currently unknown. + /// public ulong Flags; + /// + /// The original parameters used when initializing the keyboard applet. + /// public SoftwareKeyboardInitialize Initialize; + /// + /// The audio volume used by the sound effects of the keyboard. + /// public float Volume; + /// + /// The initial position of the text cursor (caret) in the provided input text. + /// public int CursorPos; + /// + /// Appearance configurations for the on-screen keyboard. + /// public SoftwareKeyboardAppear Appear; + /// + /// The initial input text to be used by the software keyboard. + /// [MarshalAs(UnmanagedType.ByValTStr, SizeConst = InputTextLength + 1)] public string InputText; - public byte Utf8Mode; + /// + /// When set, the strings communicated by software keyboard will be encoded as UTF-8 instead of UTF-16. + /// + [MarshalAs(UnmanagedType.I1)] + public bool UseUtf8; public byte Unknown3; + /// + /// [5.0.0+] Enable the backspace key in the software keyboard. + /// [MarshalAs(UnmanagedType.I1)] public bool BackspaceEnabled; @@ -41,32 +69,57 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard public byte Unknown5; [MarshalAs(UnmanagedType.I1)] - public byte KeytopAsFloating; + public bool KeytopAsFloating; [MarshalAs(UnmanagedType.I1)] - public byte FooterScalable; + public bool FooterScalable; [MarshalAs(UnmanagedType.I1)] - public byte AlphaEnabledInInputMode; + public bool AlphaEnabledInInputMode; - [MarshalAs(UnmanagedType.I1)] public byte InputModeFadeType; + /// + /// When set, the software keyboard ignores touch input. + /// [MarshalAs(UnmanagedType.I1)] - public byte TouchDisabled; + public bool TouchDisabled; + /// + /// When set, the software keyboard ignores hardware keyboard commands. + /// [MarshalAs(UnmanagedType.I1)] - public byte HardwareKeyboardDisabled; + public bool HardwareKeyboardDisabled; public uint Unknown6; public uint Unknown7; + /// + /// Default value is 1.0. + /// public float KeytopScale0; + + /// + /// Default value is 1.0. + /// public float KeytopScale1; + public float KeytopTranslate0; public float KeytopTranslate1; + + /// + /// Default value is 1.0. + /// public float KeytopBgAlpha; + + /// + /// Default value is 1.0. + /// public float FooterBgAlpha; + + /// + /// Default value is 1.0. + /// public float BalloonScale; public float Unknown8; @@ -74,9 +127,19 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard public uint Unknown10; public uint Unknown11; + /// + /// [5.0.0+] Enable sound effect. + /// public byte SeGroup; + /// + /// [6.0.0+] Enables the Trigger field when Trigger is non-zero. + /// public byte TriggerFlag; + + /// + /// [6.0.0+] Always set to zero. + /// public byte Trigger; public byte Padding; diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs new file mode 100644 index 00000000..538fb927 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// + /// A structure used by SetCustomizeDic request to software keyboard. + /// + [StructLayout(LayoutKind.Sequential, Pack = 4)] + struct SoftwareKeyboardCustomizeDic + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 112)] + public byte[] Unknown; + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs index 1abdc15b..b4ffdb90 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs @@ -2,10 +2,33 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { - [StructLayout(LayoutKind.Sequential, Pack = 4)] + /// + /// A structure with custom dictionary words for the software keyboard. + /// + [StructLayout(LayoutKind.Sequential, Pack = 2)] struct SoftwareKeyboardDictSet { - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)] - public uint[] Entries; + /// + /// A 0x1000-byte aligned buffer position. + /// + public ulong BufferPosition; + + /// + /// A 0x1000-byte aligned buffer size. + /// + public uint BufferSize; + + /// + /// Array of word entries in the buffer. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)] + public ulong[] Entries; + + /// + /// Number of used entries in the Entries field. + /// + public ushort TotalEntries; + + public ushort Padding1; } } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs index 28e3df7f..764d0e38 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs @@ -3,14 +3,23 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// - /// A structure that indicates the initialization the inline software keyboard. + /// A structure that mirrors the parameters used to initialize the keyboard applet. /// [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct SoftwareKeyboardInitialize { public uint Unknown; + + /// + /// The applet mode used when launching the swkb. The bits regarding the background vs foreground mode can be wrong. + /// public byte LibMode; + + /// + /// [5.0.0+] Set to 0x1 to indicate a firmware version >= 5.0.0. + /// public byte FivePlus; + public byte Padding1; public byte Padding2; } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs new file mode 100644 index 00000000..08f1c3d3 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// + /// A structure used by SetUserWordInfo request to the software keyboard. + /// + [StructLayout(LayoutKind.Sequential, Pack = 4)] + struct SoftwareKeyboardUserWord + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 100)] + public byte[] Unknown; + } +}