From e57b14042910eac2f876549b4a1e6fd5ba027368 Mon Sep 17 00:00:00 2001 From: Caian Benedicto Date: Mon, 11 Jan 2021 15:27:55 -0300 Subject: [PATCH] Add support for inline software keyboard (#1868) * Add background mode configuration to SoftwareKeyboardApplet * Add placeholder text generator for Software Keyboard in background mode * Add stub for GetIndirectLayerImageMap * Fix default state of DecidedCancel response * Add GUI text input to Software Keyboard in background mode * Fix graphical glitch when Inline Software Keyboard appears * Improve readability of InlineResponses class * Improve code styling and fix compiler warnings * Replace ServiceDisplay log class by ServiceVi * Replace static readonly by const * Add proper finalization to the keyboard applet in inline mode * Rename constants to start with uppercase * Fix inline keyboard not working with some games * Improve code readability * Fix code styling --- .../SoftwareKeyboard/InlineKeyboardRequest.cs | 18 ++ .../InlineKeyboardResponse.cs | 26 ++ .../SoftwareKeyboard/InlineKeyboardState.cs | 14 + .../SoftwareKeyboard/InlineResponses.cs | 295 ++++++++++++++++++ .../SoftwareKeyboardAppear.cs | 61 ++++ .../SoftwareKeyboardApplet.cs | 266 +++++++++++++--- .../SoftwareKeyboard/SoftwareKeyboardCalc.cs | 84 +++++ .../SoftwareKeyboardDictSet.cs | 11 + .../SoftwareKeyboardInitialize.cs | 17 + .../RootService/IApplicationDisplayService.cs | 19 ++ 10 files changed, 770 insertions(+), 41 deletions(-) create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs new file mode 100644 index 00000000..d0024001 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs @@ -0,0 +1,18 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// + /// Possible requests to the keyboard when running in inline mode. + /// + enum InlineKeyboardRequest : uint + { + Unknown0 = 0x0, + Finalize = 0x4, + SetUserWordInfo = 0x6, + SetCustomizeDic = 0x7, + Calc = 0xA, + SetCustomizedDictionaries = 0xB, + UnsetCustomizedDictionaries = 0xC, + UseChangedStringV2 = 0xD, + UseMovedCursorV2 = 0xE + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs new file mode 100644 index 00000000..61908b7b --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs @@ -0,0 +1,26 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// + /// Possible responses from the 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, + UnsetCustomizedDictionaries = 0xC, + ChangedStringV2 = 0xD, + MovedCursorV2 = 0xE, + ChangedStringUtf8V2 = 0xF, + MovedCursorUtf8V2 = 0x10 + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs new file mode 100644 index 00000000..2940d161 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs @@ -0,0 +1,14 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// + /// Possible states for the keyboard when running in inline mode. + /// + enum InlineKeyboardState : uint + { + Uninitialized = 0x0, + Initializing = 0x1, + Ready = 0x2, + DataAvailable = 0x3, + Completed = 0x4 + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs new file mode 100644 index 00000000..60cc5287 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs @@ -0,0 +1,295 @@ +using System.IO; +using System.Text; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + internal class InlineResponses + { + private const uint MaxStrLenUTF8 = 0x7D4; + private const uint MaxStrLenUTF16 = 0x3EC; + + private static void BeginResponse(InlineKeyboardState state, InlineKeyboardResponse resCode, BinaryWriter writer) + { + writer.Write((uint)state); + writer.Write((uint)resCode); + } + + private static uint WriteString(string text, BinaryWriter writer, uint maxSize, Encoding encoding) + { + // Ensure the text fits in the buffer, but do not straight cut the bytes because + // this may corrupt the encoding. Search for a cut in the source string that fits. + + byte[] bytes = null; + + for (int maxStr = text.Length; maxStr >= 0; maxStr--) + { + // This loop will probably will run only once. + bytes = encoding.GetBytes(text.Substring(0, maxStr)); + if (bytes.Length <= maxSize) + { + break; + } + } + + writer.Write(bytes); + writer.Seek((int)maxSize - bytes.Length, SeekOrigin.Current); + writer.Write((uint)text.Length); // String size + + return (uint)text.Length; // Return the cursor position at the end of the text + } + + private static void WriteStringWithCursor(string text, BinaryWriter writer, uint maxSize, Encoding encoding) + { + uint cursor = WriteString(text, writer, maxSize, encoding); + + writer.Write(cursor); // Cursor position + } + + public static byte[] FinishedInitialize() + { + 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); + writer.Write((byte)1); // Data (ignored by the program) + + return stream.ToArray(); + } + } + + public static byte[] Default() + { + 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); + + return stream.ToArray(); + } + } + + public static byte[] ChangedString(string text) + { + uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.ChangedString, writer); + WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); + writer.Write((int)0); // ? + writer.Write((int)0); // ? + + return stream.ToArray(); + } + } + + public static byte[] MovedCursor(string text) + { + 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); + WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); + + return stream.ToArray(); + } + } + + public static byte[] MovedTab(string text) + { + // Should be the same as MovedCursor. + + uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.MovedTab, writer); + WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); + + return stream.ToArray(); + } + } + + public static byte[] DecidedEnter(string text) + { + 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); + WriteString(text, writer, MaxStrLenUTF16, Encoding.Unicode); + + return stream.ToArray(); + } + } + + public static byte[] DecidedCancel() + { + 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); + + return stream.ToArray(); + } + } + + public static byte[] ChangedStringUtf8(string text) + { + 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); + WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); + writer.Write((int)0); // ? + writer.Write((int)0); // ? + + return stream.ToArray(); + } + } + + public static byte[] MovedCursorUtf8(string text) + { + 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); + WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); + + return stream.ToArray(); + } + } + + public static byte[] DecidedEnterUtf8(string text) + { + 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); + WriteString(text, writer, MaxStrLenUTF8, Encoding.UTF8); + + return stream.ToArray(); + } + } + + public static byte[] UnsetCustomizeDic() + { + 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); + + return stream.ToArray(); + } + } + + public static byte[] ReleasedUserWordInfo() + { + 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); + + return stream.ToArray(); + } + } + + public static byte[] UnsetCustomizedDictionaries() + { + 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); + + return stream.ToArray(); + } + } + + public static byte[] ChangedStringV2(string text) + { + 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); + WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); + writer.Write((int)0); // ? + writer.Write((int)0); // ? + writer.Write((byte)0); // Flag == 0 + + return stream.ToArray(); + } + } + + public static byte[] MovedCursorV2(string text) + { + 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); + WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode); + writer.Write((byte)0); // Flag == 0 + + return stream.ToArray(); + } + } + + public static byte[] ChangedStringUtf8V2(string text) + { + 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); + WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); + writer.Write((int)0); // ? + writer.Write((int)0); // ? + writer.Write((byte)0); // Flag == 0 + + return stream.ToArray(); + } + } + + public static byte[] MovedCursorUtf8V2(string text) + { + 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); + WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8); + writer.Write((byte)0); // Flag == 0 + + return stream.ToArray(); + } + } + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs new file mode 100644 index 00000000..23e8bd1f --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs @@ -0,0 +1,61 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + [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. + public uint ShouldBeHidden; + + [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. + /// + [MarshalAs(UnmanagedType.I1)] + public bool PredictionEnabled; + + public byte Empty; + + /// + /// Specifies prohibited characters that cannot be input into the text entry area. + /// + public InvalidCharFlags InvalidCharFlag; + + public int Padding1; + public int Padding2; + + public byte EnableReturnButton; + + public byte Padding3; + public byte Padding4; + public byte Padding5; + + public uint CalcArgFlags; + + public uint Padding6; + public uint Padding7; + public uint Padding8; + public uint Padding9; + public uint Padding10; + public uint Padding11; + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs index cec466ca..89ed5592 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs @@ -5,6 +5,8 @@ using System; using System.IO; using System.Runtime.InteropServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace Ryujinx.HLE.HOS.Applets { @@ -19,10 +21,19 @@ namespace Ryujinx.HLE.HOS.Applets private SoftwareKeyboardState _state = SoftwareKeyboardState.Uninitialized; + private bool _isBackground = false; + private AppletSession _normalSession; private AppletSession _interactiveSession; - private SoftwareKeyboardConfig _keyboardConfig; + // Configuration for foreground mode + private SoftwareKeyboardConfig _keyboardFgConfig; + private SoftwareKeyboardCalc _keyboardCalc; + private SoftwareKeyboardDictSet _keyboardDict; + + // Configuration for background mode + private SoftwareKeyboardInitialize _keyboardBgInitialize; + private byte[] _transferMemory; private string _textValue = null; @@ -47,30 +58,46 @@ namespace Ryujinx.HLE.HOS.Applets var launchParams = _normalSession.Pop(); var keyboardConfig = _normalSession.Pop(); - if (keyboardConfig.Length < Marshal.SizeOf()) + // TODO: A better way would be handling the background creation properly + // in LibraryAppleCreator / Acessor instead of guessing by size. + if (keyboardConfig.Length == Marshal.SizeOf()) { - Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf():x}. Got {keyboardConfig.Length:x}"); + _isBackground = true; + + _keyboardBgInitialize = ReadStruct(keyboardConfig); + _state = SoftwareKeyboardState.Uninitialized; + + return ResultCode.Success; } else { - _keyboardConfig = ReadStruct(keyboardConfig); + _isBackground = false; + + if (keyboardConfig.Length < Marshal.SizeOf()) + { + Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf():x}. Got {keyboardConfig.Length:x}"); + } + else + { + _keyboardFgConfig = ReadStruct(keyboardConfig); + } + + if (!_normalSession.TryPop(out _transferMemory)) + { + Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null"); + } + + if (_keyboardFgConfig.UseUtf8) + { + _encoding = Encoding.UTF8; + } + + _state = SoftwareKeyboardState.Ready; + + ExecuteForegroundKeyboard(); + + return ResultCode.Success; } - - if (!_normalSession.TryPop(out _transferMemory)) - { - Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null"); - } - - if (_keyboardConfig.UseUtf8) - { - _encoding = Encoding.UTF8; - } - - _state = SoftwareKeyboardState.Ready; - - Execute(); - - return ResultCode.Success; } public ResultCode GetResult() @@ -78,39 +105,39 @@ namespace Ryujinx.HLE.HOS.Applets return ResultCode.Success; } - private void Execute() + 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 && _keyboardConfig.InitialStringLength > 0) + if (_transferMemory != null && _keyboardFgConfig.InitialStringLength > 0) { - initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardConfig.InitialStringOffset, 2 * _keyboardConfig.InitialStringLength); + initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardFgConfig.InitialStringOffset, 2 * _keyboardFgConfig.InitialStringLength); } // If the max string length is 0, we set it to a large default // length. - if (_keyboardConfig.StringLengthMax == 0) + if (_keyboardFgConfig.StringLengthMax == 0) { - _keyboardConfig.StringLengthMax = 100; + _keyboardFgConfig.StringLengthMax = 100; } var args = new SoftwareKeyboardUiArgs { - HeaderText = _keyboardConfig.HeaderText, - SubtitleText = _keyboardConfig.SubtitleText, - GuideText = _keyboardConfig.GuideText, - SubmitText = (!string.IsNullOrWhiteSpace(_keyboardConfig.SubmitText) ? _keyboardConfig.SubmitText : "OK"), - StringLengthMin = _keyboardConfig.StringLengthMin, - StringLengthMax = _keyboardConfig.StringLengthMax, + HeaderText = _keyboardFgConfig.HeaderText, + SubtitleText = _keyboardFgConfig.SubtitleText, + GuideText = _keyboardFgConfig.GuideText, + SubmitText = (!string.IsNullOrWhiteSpace(_keyboardFgConfig.SubmitText) ? _keyboardFgConfig.SubmitText : "OK"), + StringLengthMin = _keyboardFgConfig.StringLengthMin, + StringLengthMax = _keyboardFgConfig.StringLengthMax, InitialText = initialText }; // Call the configured GUI handler to get user's input if (_device.UiHandler == null) { - Logger.Warning?.Print(LogClass.Application, $"GUI Handler is not set. Falling back to default"); + Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default"); _okPressed = true; } else @@ -122,22 +149,22 @@ namespace Ryujinx.HLE.HOS.Applets // If the game requests a string with a minimum length less // than our default text, repeat our default text until we meet - // the minimum length requirement. + // the minimum length requirement. // This should always be done before the text truncation step. - while (_textValue.Length < _keyboardConfig.StringLengthMin) + while (_textValue.Length < _keyboardFgConfig.StringLengthMin) { _textValue = String.Join(" ", _textValue, _textValue); } // If our default text is longer than the allowed length, // we truncate it. - if (_textValue.Length > _keyboardConfig.StringLengthMax) + if (_textValue.Length > _keyboardFgConfig.StringLengthMax) { - _textValue = _textValue.Substring(0, (int)_keyboardConfig.StringLengthMax); + _textValue = _textValue.Substring(0, (int)_keyboardFgConfig.StringLengthMax); } // Does the application want to validate the text itself? - if (_keyboardConfig.CheckText) + if (_keyboardFgConfig.CheckText) { // The application needs to validate the response, so we // submit it to the interactive output buffer, and poll it @@ -151,7 +178,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. + // and poll it for completion. _state = SoftwareKeyboardState.Complete; _normalSession.Push(BuildResponse(_textValue, false)); @@ -162,16 +189,28 @@ namespace Ryujinx.HLE.HOS.Applets private void OnInteractiveData(object sender, EventArgs e) { - // Obtain the validation status response, + // Obtain the validation status response. var data = _interactiveSession.Pop(); + if (_isBackground) + { + OnBackgroundInteractiveData(data); + } + else + { + OnForegroundInteractiveData(data); + } + } + + private void OnForegroundInteractiveData(byte[] data) + { if (_state == SoftwareKeyboardState.ValidationPending) { // TODO(jduncantor): // If application rejects our "attempt", submit another attempt, // and put the applet back in PendingValidation state. - // For now we assume success, so we push the final result + // For now we assume success, so we push the final result // to the standard output buffer and carry on our merry way. _normalSession.Push(BuildResponse(_textValue, false)); @@ -194,6 +233,151 @@ namespace Ryujinx.HLE.HOS.Applets } } + private void OnBackgroundInteractiveData(byte[] data) + { + // WARNING: Only invoke applet state changes after an explicit finalization + // request from the game, this is because the inline keyboard is expected to + // keep running in the background sending data by itself. + + using (MemoryStream stream = new MemoryStream(data)) + using (BinaryReader reader = new BinaryReader(stream)) + { + var request = (InlineKeyboardRequest)reader.ReadUInt32(); + + long remaining; + + // Always show the keyboard if the state is 'Ready'. + bool showKeyboard = _state == SoftwareKeyboardState.Ready; + + 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()); + break; + case InlineKeyboardRequest.UseMovedCursorV2: + // Not used because we only send the entire string after confirmation. + _interactiveSession.Push(InlineResponses.Default()); + break; + case InlineKeyboardRequest.SetCustomizeDic: + remaining = stream.Length - stream.Position; + if (remaining != Marshal.SizeOf()) + { + Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes!"); + } + else + { + var keyboardDictData = reader.ReadBytes((int)remaining); + _keyboardDict = ReadStruct(keyboardDictData); + } + _interactiveSession.Push(InlineResponses.Default()); + break; + case InlineKeyboardRequest.Calc: + // Put the keyboard in a Ready state, this will force showing + _state = SoftwareKeyboardState.Ready; + remaining = stream.Length - stream.Position; + if (remaining != Marshal.SizeOf()) + { + Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes!"); + } + else + { + var keyboardCalcData = reader.ReadBytes((int)remaining); + _keyboardCalc = ReadStruct(keyboardCalcData); + + if (_keyboardCalc.Utf8Mode == 0x1) + { + _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) + { + showKeyboard = true; + } + } + // Send an initialization finished signal. + _interactiveSession.Push(InlineResponses.FinishedInitialize()); + // 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(); + break; + case InlineKeyboardRequest.Finalize: + // The game wants to close the keyboard applet and will wait for a state change. + _state = SoftwareKeyboardState.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()); + break; + } + } + } + private byte[] BuildResponse(string text, bool interactive) { int bufferSize = interactive ? InteractiveBufferSize : StandardBufferSize; @@ -227,7 +411,7 @@ namespace Ryujinx.HLE.HOS.Applets GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned); try - { + { return Marshal.PtrToStructure(handle.AddrOfPinnedObject()); } finally diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs new file mode 100644 index 00000000..6213c2e4 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs @@ -0,0 +1,84 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// + /// A structure that defines the configuration options of the software keyboard. + /// + [StructLayout(LayoutKind.Sequential, Pack=1, CharSet = CharSet.Unicode)] + struct SoftwareKeyboardCalc + { + private const int InputTextLength = 505; + + public uint Unknown; + + public ushort Size; + + public byte Unknown1; + public byte Unknown2; + + public ulong Flags; + + public SoftwareKeyboardInitialize Initialize; + + public float Volume; + + public int CursorPos; + + public SoftwareKeyboardAppear Appear; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = InputTextLength + 1)] + public string InputText; + + public byte Utf8Mode; + + public byte Unknown3; + + [MarshalAs(UnmanagedType.I1)] + public bool BackspaceEnabled; + + public short Unknown4; + public byte Unknown5; + + [MarshalAs(UnmanagedType.I1)] + public byte KeytopAsFloating; + + [MarshalAs(UnmanagedType.I1)] + public byte FooterScalable; + + [MarshalAs(UnmanagedType.I1)] + public byte AlphaEnabledInInputMode; + + [MarshalAs(UnmanagedType.I1)] + public byte InputModeFadeType; + + [MarshalAs(UnmanagedType.I1)] + public byte TouchDisabled; + + [MarshalAs(UnmanagedType.I1)] + public byte HardwareKeyboardDisabled; + + public uint Unknown6; + public uint Unknown7; + + public float KeytopScale0; + public float KeytopScale1; + public float KeytopTranslate0; + public float KeytopTranslate1; + public float KeytopBgAlpha; + public float FooterBgAlpha; + public float BalloonScale; + + public float Unknown8; + public uint Unknown9; + public uint Unknown10; + public uint Unknown11; + + public byte SeGroup; + + public byte TriggerFlag; + public byte Trigger; + + public byte Padding; + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs new file mode 100644 index 00000000..1abdc15b --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + [StructLayout(LayoutKind.Sequential, Pack = 4)] + struct SoftwareKeyboardDictSet + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)] + public uint[] Entries; + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs new file mode 100644 index 00000000..28e3df7f --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// + /// A structure that indicates the initialization the inline software keyboard. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + struct SoftwareKeyboardInitialize + { + public uint Unknown; + public byte LibMode; + public byte FivePlus; + public byte Padding1; + public byte Padding2; + } +} diff --git a/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs b/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs index a4b9f358..b521ae92 100644 --- a/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs +++ b/Ryujinx.HLE/HOS/Services/Vi/RootService/IApplicationDisplayService.cs @@ -1,4 +1,5 @@ using Ryujinx.Common; +using Ryujinx.Common.Logging; using Ryujinx.Cpu; using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Kernel.Common; @@ -238,6 +239,24 @@ namespace Ryujinx.HLE.HOS.Services.Vi.RootService return null; } + [Command(2450)] + // GetIndirectLayerImageMap(s64 width, s64 height, u64 handle, nn::applet::AppletResourceUserId, pid) -> (s64, s64, buffer) + public ResultCode GetIndirectLayerImageMap(ServiceCtx context) + { + // The size of the layer buffer should be an aligned multiple of width * height + // because it was created using GetIndirectLayerImageRequiredMemoryInfo as a guide. + + long layerBuffPosition = context.Request.ReceiveBuff[0].Position; + long layerBuffSize = context.Request.ReceiveBuff[0].Size; + + // Fill the layer with zeros. + context.Memory.Fill((ulong)layerBuffPosition, (ulong)layerBuffSize, 0x00); + + Logger.Stub?.PrintStub(LogClass.ServiceVi); + + return ResultCode.Success; + } + [Command(2460)] // GetIndirectLayerImageRequiredMemoryInfo(u64 width, u64 height) -> (u64 size, u64 alignment) public ResultCode GetIndirectLayerImageRequiredMemoryInfo(ServiceCtx context)