/ src / Ryujinx.HLE / HOS / Applets / SoftwareKeyboard / SoftwareKeyboardApplet.cs
SoftwareKeyboardApplet.cs
  1  using Ryujinx.Common;
  2  using Ryujinx.Common.Configuration.Hid;
  3  using Ryujinx.Common.Logging;
  4  using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
  5  using Ryujinx.HLE.HOS.Services.Am.AppletAE;
  6  using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad;
  7  using Ryujinx.HLE.UI;
  8  using Ryujinx.HLE.UI.Input;
  9  using Ryujinx.Memory;
 10  using System;
 11  using System.Diagnostics;
 12  using System.Diagnostics.CodeAnalysis;
 13  using System.IO;
 14  using System.Runtime.CompilerServices;
 15  using System.Runtime.InteropServices;
 16  using System.Text;
 17  
 18  namespace Ryujinx.HLE.HOS.Applets
 19  {
 20      internal class SoftwareKeyboardApplet : IApplet
 21      {
 22          private const string DefaultInputText = "Ryujinx";
 23  
 24          private const int StandardBufferSize = 0x7D8;
 25          private const int InteractiveBufferSize = 0x7D4;
 26          private const int MaxUserWords = 0x1388;
 27          private const int MaxUiTextSize = 100;
 28  
 29          private const Key CycleInputModesKey = Key.F6;
 30  
 31          private readonly Switch _device;
 32  
 33          private SoftwareKeyboardState _foregroundState = SoftwareKeyboardState.Uninitialized;
 34          private volatile InlineKeyboardState _backgroundState = InlineKeyboardState.Uninitialized;
 35  
 36          private bool _isBackground = false;
 37  
 38          private AppletSession _normalSession;
 39          private AppletSession _interactiveSession;
 40  
 41          // Configuration for foreground mode.
 42          private SoftwareKeyboardConfig _keyboardForegroundConfig;
 43  
 44          // Configuration for background (inline) mode.
 45  #pragma warning disable IDE0052 // Remove unread private member
 46          private SoftwareKeyboardInitialize _keyboardBackgroundInitialize;
 47          private SoftwareKeyboardCustomizeDic _keyboardBackgroundDic;
 48          private SoftwareKeyboardDictSet _keyboardBackgroundDictSet;
 49  #pragma warning restore IDE0052
 50          private SoftwareKeyboardUserWord[] _keyboardBackgroundUserWords;
 51  
 52          private byte[] _transferMemory;
 53  
 54          private string _textValue = "";
 55          private int _cursorBegin = 0;
 56          private Encoding _encoding = Encoding.Unicode;
 57          private KeyboardResult _lastResult = KeyboardResult.NotSet;
 58  
 59          private IDynamicTextInputHandler _dynamicTextInputHandler = null;
 60          private SoftwareKeyboardRenderer _keyboardRenderer = null;
 61          private NpadReader _npads = null;
 62          private bool _canAcceptController = false;
 63          private KeyboardInputMode _inputMode = KeyboardInputMode.ControllerAndKeyboard;
 64  
 65          private readonly object _lock = new();
 66  
 67          public event EventHandler AppletStateChanged;
 68  
 69          public SoftwareKeyboardApplet(Horizon system)
 70          {
 71              _device = system.Device;
 72          }
 73  
 74          public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
 75          {
 76              lock (_lock)
 77              {
 78                  _normalSession = normalSession;
 79                  _interactiveSession = interactiveSession;
 80  
 81                  _interactiveSession.DataAvailable += OnInteractiveData;
 82  
 83                  var launchParams = _normalSession.Pop();
 84                  var keyboardConfig = _normalSession.Pop();
 85  
 86                  _isBackground = keyboardConfig.Length == Unsafe.SizeOf<SoftwareKeyboardInitialize>();
 87  
 88                  if (_isBackground)
 89                  {
 90                      // Initialize the keyboard applet in background mode.
 91  
 92                      _keyboardBackgroundInitialize = MemoryMarshal.Read<SoftwareKeyboardInitialize>(keyboardConfig);
 93                      _backgroundState = InlineKeyboardState.Uninitialized;
 94  
 95                      if (_device.UIHandler == null)
 96                      {
 97                          Logger.Error?.Print(LogClass.ServiceAm, "GUI Handler is not set, software keyboard applet will not work properly");
 98                      }
 99                      else
100                      {
101                          // Create a text handler that converts keyboard strokes to strings.
102                          _dynamicTextInputHandler = _device.UIHandler.CreateDynamicTextInputHandler();
103                          _dynamicTextInputHandler.TextChangedEvent += HandleTextChangedEvent;
104                          _dynamicTextInputHandler.KeyPressedEvent += HandleKeyPressedEvent;
105  
106                          _npads = new NpadReader(_device);
107                          _npads.NpadButtonDownEvent += HandleNpadButtonDownEvent;
108                          _npads.NpadButtonUpEvent += HandleNpadButtonUpEvent;
109  
110                          _keyboardRenderer = new SoftwareKeyboardRenderer(_device.UIHandler.HostUITheme);
111                      }
112  
113                      return ResultCode.Success;
114                  }
115                  else
116                  {
117                      // Initialize the keyboard applet in foreground mode.
118  
119                      if (keyboardConfig.Length < Marshal.SizeOf<SoftwareKeyboardConfig>())
120                      {
121                          Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf<SoftwareKeyboardConfig>():x}. Got {keyboardConfig.Length:x}");
122                      }
123                      else
124                      {
125                          _keyboardForegroundConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
126                      }
127  
128                      if (!_normalSession.TryPop(out _transferMemory))
129                      {
130                          Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null");
131                      }
132  
133                      if (_keyboardForegroundConfig.UseUtf8)
134                      {
135                          _encoding = Encoding.UTF8;
136                      }
137  
138                      _foregroundState = SoftwareKeyboardState.Ready;
139  
140                      ExecuteForegroundKeyboard();
141  
142                      return ResultCode.Success;
143                  }
144              }
145          }
146  
147          public ResultCode GetResult()
148          {
149              return ResultCode.Success;
150          }
151  
152          private bool IsKeyboardActive()
153          {
154              return _backgroundState >= InlineKeyboardState.Appearing && _backgroundState < InlineKeyboardState.Disappearing;
155          }
156  
157          private bool InputModeControllerEnabled()
158          {
159              return _inputMode == KeyboardInputMode.ControllerAndKeyboard ||
160                     _inputMode == KeyboardInputMode.ControllerOnly;
161          }
162  
163          private bool InputModeTypingEnabled()
164          {
165              return _inputMode == KeyboardInputMode.ControllerAndKeyboard ||
166                     _inputMode == KeyboardInputMode.KeyboardOnly;
167          }
168  
169          private void AdvanceInputMode()
170          {
171              _inputMode = (KeyboardInputMode)((int)(_inputMode + 1) % (int)KeyboardInputMode.Count);
172          }
173  
174          public bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position)
175          {
176              _npads?.Update();
177  
178              _keyboardRenderer?.SetSurfaceInfo(surfaceInfo);
179  
180              return _keyboardRenderer?.DrawTo(destination, position) ?? false;
181          }
182  
183          private void ExecuteForegroundKeyboard()
184          {
185              string initialText = null;
186  
187              // Initial Text is always encoded as a UTF-16 string in the work buffer (passed as transfer memory)
188              // InitialStringOffset points to the memory offset and InitialStringLength is the number of UTF-16 characters
189              if (_transferMemory != null && _keyboardForegroundConfig.InitialStringLength > 0)
190              {
191                  initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardForegroundConfig.InitialStringOffset,
192                      2 * _keyboardForegroundConfig.InitialStringLength);
193              }
194  
195              // If the max string length is 0, we set it to a large default
196              // length.
197              if (_keyboardForegroundConfig.StringLengthMax == 0)
198              {
199                  _keyboardForegroundConfig.StringLengthMax = 100;
200              }
201  
202              if (_device.UIHandler == null)
203              {
204                  Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default");
205  
206                  _textValue = DefaultInputText;
207                  _lastResult = KeyboardResult.Accept;
208              }
209              else
210              {
211                  // Call the configured GUI handler to get user's input.
212                  var args = new SoftwareKeyboardUIArgs
213                  {
214                      KeyboardMode = _keyboardForegroundConfig.Mode,
215                      HeaderText = StripUnicodeControlCodes(_keyboardForegroundConfig.HeaderText),
216                      SubtitleText = StripUnicodeControlCodes(_keyboardForegroundConfig.SubtitleText),
217                      GuideText = StripUnicodeControlCodes(_keyboardForegroundConfig.GuideText),
218                      SubmitText = (!string.IsNullOrWhiteSpace(_keyboardForegroundConfig.SubmitText) ?
219                      _keyboardForegroundConfig.SubmitText : "OK"),
220                      StringLengthMin = _keyboardForegroundConfig.StringLengthMin,
221                      StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
222                      InitialText = initialText,
223                  };
224  
225                  _lastResult = _device.UIHandler.DisplayInputDialog(args, out _textValue) ? KeyboardResult.Accept : KeyboardResult.Cancel;
226                  _textValue ??= initialText ?? DefaultInputText;
227              }
228  
229              // If the game requests a string with a minimum length less
230              // than our default text, repeat our default text until we meet
231              // the minimum length requirement.
232              // This should always be done before the text truncation step.
233              while (_textValue.Length < _keyboardForegroundConfig.StringLengthMin)
234              {
235                  _textValue = String.Join(" ", _textValue, _textValue);
236              }
237  
238              // If our default text is longer than the allowed length,
239              // we truncate it.
240              if (_textValue.Length > _keyboardForegroundConfig.StringLengthMax)
241              {
242                  _textValue = _textValue[.._keyboardForegroundConfig.StringLengthMax];
243              }
244  
245              // Does the application want to validate the text itself?
246              if (_keyboardForegroundConfig.CheckText)
247              {
248                  // The application needs to validate the response, so we
249                  // submit it to the interactive output buffer, and poll it
250                  // for validation. Once validated, the application will submit
251                  // back a validation status, which is handled in OnInteractiveDataPushIn.
252                  _foregroundState = SoftwareKeyboardState.ValidationPending;
253  
254                  PushForegroundResponse(true);
255              }
256              else
257              {
258                  // If the application doesn't need to validate the response,
259                  // we push the data to the non-interactive output buffer
260                  // and poll it for completion.
261                  _foregroundState = SoftwareKeyboardState.Complete;
262  
263                  PushForegroundResponse(false);
264  
265                  AppletStateChanged?.Invoke(this, null);
266              }
267          }
268  
269          private void OnInteractiveData(object sender, EventArgs e)
270          {
271              // Obtain the validation status response.
272              var data = _interactiveSession.Pop();
273  
274              if (_isBackground)
275              {
276                  lock (_lock)
277                  {
278                      OnBackgroundInteractiveData(data);
279                  }
280              }
281              else
282              {
283                  OnForegroundInteractiveData(data);
284              }
285          }
286  
287          private void OnForegroundInteractiveData(byte[] data)
288          {
289              if (_foregroundState == SoftwareKeyboardState.ValidationPending)
290              {
291                  // TODO(jduncantor):
292                  // If application rejects our "attempt", submit another attempt,
293                  // and put the applet back in PendingValidation state.
294  
295                  // For now we assume success, so we push the final result
296                  // to the standard output buffer and carry on our merry way.
297                  PushForegroundResponse(false);
298  
299                  AppletStateChanged?.Invoke(this, null);
300  
301                  _foregroundState = SoftwareKeyboardState.Complete;
302              }
303              else if (_foregroundState == SoftwareKeyboardState.Complete)
304              {
305                  // If we have already completed, we push the result text
306                  // back on the output buffer and poll the application.
307                  PushForegroundResponse(false);
308  
309                  AppletStateChanged?.Invoke(this, null);
310              }
311              else
312              {
313                  // We shouldn't be able to get here through standard swkbd execution.
314                  throw new InvalidOperationException("Software Keyboard is in an invalid state.");
315              }
316          }
317  
318          private void OnBackgroundInteractiveData(byte[] data)
319          {
320              // WARNING: Only invoke applet state changes after an explicit finalization
321              // request from the game, this is because the inline keyboard is expected to
322              // keep running in the background sending data by itself.
323  
324              using MemoryStream stream = new(data);
325              using BinaryReader reader = new(stream);
326  
327              var request = (InlineKeyboardRequest)reader.ReadUInt32();
328  
329              long remaining;
330  
331              Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {_backgroundState}");
332  
333              switch (request)
334              {
335                  case InlineKeyboardRequest.UseChangedStringV2:
336                      Logger.Stub?.Print(LogClass.ServiceAm, "Inline keyboard request UseChangedStringV2");
337                      break;
338                  case InlineKeyboardRequest.UseMovedCursorV2:
339                      Logger.Stub?.Print(LogClass.ServiceAm, "Inline keyboard request UseMovedCursorV2");
340                      break;
341                  case InlineKeyboardRequest.SetUserWordInfo:
342                      // Read the user word info data.
343                      remaining = stream.Length - stream.Position;
344                      if (remaining < sizeof(int))
345                      {
346                          Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes");
347                      }
348                      else
349                      {
350                          int wordsCount = reader.ReadInt32();
351                          int wordSize = Unsafe.SizeOf<SoftwareKeyboardUserWord>();
352                          remaining = stream.Length - stream.Position;
353  
354                          if (wordsCount > MaxUserWords)
355                          {
356                              Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}");
357                          }
358                          else if (wordsCount * wordSize != remaining)
359                          {
360                              Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words");
361                          }
362                          else
363                          {
364                              _keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount];
365  
366                              for (int word = 0; word < wordsCount; word++)
367                              {
368                                  _keyboardBackgroundUserWords[word] = reader.ReadStruct<SoftwareKeyboardUserWord>();
369                              }
370                          }
371                      }
372                      _interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(_backgroundState));
373                      break;
374                  case InlineKeyboardRequest.SetCustomizeDic:
375                      // Read the custom dic data.
376                      remaining = stream.Length - stream.Position;
377                      if (remaining != Unsafe.SizeOf<SoftwareKeyboardCustomizeDic>())
378                      {
379                          Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes");
380                      }
381                      else
382                      {
383                          _keyboardBackgroundDic = reader.ReadStruct<SoftwareKeyboardCustomizeDic>();
384                      }
385                      break;
386                  case InlineKeyboardRequest.SetCustomizedDictionaries:
387                      // Read the custom dictionaries data.
388                      remaining = stream.Length - stream.Position;
389                      if (remaining != Unsafe.SizeOf<SoftwareKeyboardDictSet>())
390                      {
391                          Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes");
392                      }
393                      else
394                      {
395                          _keyboardBackgroundDictSet = reader.ReadStruct<SoftwareKeyboardDictSet>();
396                      }
397                      break;
398                  case InlineKeyboardRequest.Calc:
399                      // The Calc request is used to communicate configuration changes and commands to the keyboard.
400                      // Fields in the Calc struct and operations are masked by the Flags field.
401  
402                      // Read the Calc data.
403                      SoftwareKeyboardCalcEx newCalc;
404                      remaining = stream.Length - stream.Position;
405                      if (remaining == Marshal.SizeOf<SoftwareKeyboardCalc>())
406                      {
407                          var keyboardCalcData = reader.ReadBytes((int)remaining);
408                          var keyboardCalc = ReadStruct<SoftwareKeyboardCalc>(keyboardCalcData);
409  
410                          newCalc = keyboardCalc.ToExtended();
411                      }
412                      else if (remaining == Marshal.SizeOf<SoftwareKeyboardCalcEx>() || remaining == SoftwareKeyboardCalcEx.AlternativeSize)
413                      {
414                          var keyboardCalcData = reader.ReadBytes((int)remaining);
415  
416                          newCalc = ReadStruct<SoftwareKeyboardCalcEx>(keyboardCalcData);
417                      }
418                      else
419                      {
420                          Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes");
421  
422                          newCalc = new SoftwareKeyboardCalcEx();
423                      }
424  
425                      // Process each individual operation specified in the flags.
426  
427                      bool updateText = false;
428  
429                      if ((newCalc.Flags & KeyboardCalcFlags.Initialize) != 0)
430                      {
431                          _interactiveSession.Push(InlineResponses.FinishedInitialize(_backgroundState));
432  
433                          _backgroundState = InlineKeyboardState.Initialized;
434                      }
435  
436                      if ((newCalc.Flags & KeyboardCalcFlags.SetCursorPos) != 0)
437                      {
438                          _cursorBegin = newCalc.CursorPos;
439                          updateText = true;
440  
441                          Logger.Debug?.Print(LogClass.ServiceAm, $"Cursor position set to {_cursorBegin}");
442                      }
443  
444                      if ((newCalc.Flags & KeyboardCalcFlags.SetInputText) != 0)
445                      {
446                          _textValue = newCalc.InputText;
447                          updateText = true;
448  
449                          Logger.Debug?.Print(LogClass.ServiceAm, $"Input text set to {_textValue}");
450                      }
451  
452                      if ((newCalc.Flags & KeyboardCalcFlags.SetUtf8Mode) != 0)
453                      {
454                          _encoding = newCalc.UseUtf8 ? Encoding.UTF8 : Encoding.Default;
455  
456                          Logger.Debug?.Print(LogClass.ServiceAm, $"Encoding set to {_encoding}");
457                      }
458  
459                      if (updateText)
460                      {
461                          _dynamicTextInputHandler.SetText(_textValue, _cursorBegin);
462                          _keyboardRenderer.UpdateTextState(_textValue, _cursorBegin, _cursorBegin, null, null);
463                      }
464  
465                      if ((newCalc.Flags & KeyboardCalcFlags.MustShow) != 0)
466                      {
467                          ActivateFrontend();
468  
469                          _backgroundState = InlineKeyboardState.Shown;
470  
471                          PushChangedString(_textValue, (uint)_cursorBegin, _backgroundState);
472                      }
473  
474                      // Send the response to the Calc
475                      _interactiveSession.Push(InlineResponses.Default(_backgroundState));
476                      break;
477                  case InlineKeyboardRequest.Finalize:
478                      // Destroy the frontend.
479                      DestroyFrontend();
480                      // The calling application wants to close the keyboard applet and will wait for a state change.
481                      _backgroundState = InlineKeyboardState.Uninitialized;
482                      AppletStateChanged?.Invoke(this, null);
483                      break;
484                  default:
485                      // We shouldn't be able to get here through standard swkbd execution.
486                      Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_backgroundState}");
487                      _interactiveSession.Push(InlineResponses.Default(_backgroundState));
488                      break;
489              }
490          }
491  
492          private void ActivateFrontend()
493          {
494              Logger.Debug?.Print(LogClass.ServiceAm, "Activating software keyboard frontend");
495  
496              _inputMode = KeyboardInputMode.ControllerAndKeyboard;
497  
498              _npads.Update(true);
499  
500              NpadButton buttons = _npads.GetCurrentButtonsOfAllNpads();
501  
502              // Block the input if the current accept key is pressed so the applet won't be instantly closed.
503              _canAcceptController = (buttons & NpadButton.A) == 0;
504  
505              _dynamicTextInputHandler.TextProcessingEnabled = true;
506  
507              _keyboardRenderer.UpdateCommandState(null, null, true);
508              _keyboardRenderer.UpdateTextState(null, null, null, null, true);
509          }
510  
511          private void DeactivateFrontend()
512          {
513              Logger.Debug?.Print(LogClass.ServiceAm, "Deactivating software keyboard frontend");
514  
515              _inputMode = KeyboardInputMode.ControllerAndKeyboard;
516              _canAcceptController = false;
517  
518              _dynamicTextInputHandler.TextProcessingEnabled = false;
519              _dynamicTextInputHandler.SetText(_textValue, _cursorBegin);
520          }
521  
522          private void DestroyFrontend()
523          {
524              Logger.Debug?.Print(LogClass.ServiceAm, "Destroying software keyboard frontend");
525  
526              _keyboardRenderer?.Dispose();
527              _keyboardRenderer = null;
528  
529              if (_dynamicTextInputHandler != null)
530              {
531                  _dynamicTextInputHandler.TextChangedEvent -= HandleTextChangedEvent;
532                  _dynamicTextInputHandler.KeyPressedEvent -= HandleKeyPressedEvent;
533                  _dynamicTextInputHandler.Dispose();
534                  _dynamicTextInputHandler = null;
535              }
536  
537              if (_npads != null)
538              {
539                  _npads.NpadButtonDownEvent -= HandleNpadButtonDownEvent;
540                  _npads.NpadButtonUpEvent -= HandleNpadButtonUpEvent;
541                  _npads = null;
542              }
543          }
544  
545          private bool HandleKeyPressedEvent(Key key)
546          {
547              if (key == CycleInputModesKey)
548              {
549                  lock (_lock)
550                  {
551                      if (IsKeyboardActive())
552                      {
553                          AdvanceInputMode();
554  
555                          bool typingEnabled = InputModeTypingEnabled();
556                          bool controllerEnabled = InputModeControllerEnabled();
557  
558                          _dynamicTextInputHandler.TextProcessingEnabled = typingEnabled;
559  
560                          _keyboardRenderer.UpdateTextState(null, null, null, null, typingEnabled);
561                          _keyboardRenderer.UpdateCommandState(null, null, controllerEnabled);
562                      }
563                  }
564              }
565  
566              return true;
567          }
568  
569          private void HandleTextChangedEvent(string text, int cursorBegin, int cursorEnd, bool overwriteMode)
570          {
571              lock (_lock)
572              {
573                  // Text processing should not run with typing disabled.
574                  Debug.Assert(InputModeTypingEnabled());
575  
576                  if (text.Length > MaxUiTextSize)
577                  {
578                      // Limit the text size and change it back.
579                      text = text[..MaxUiTextSize];
580                      cursorBegin = Math.Min(cursorBegin, MaxUiTextSize);
581                      cursorEnd = Math.Min(cursorEnd, MaxUiTextSize);
582  
583                      _dynamicTextInputHandler.SetText(text, cursorBegin, cursorEnd);
584                  }
585  
586                  _textValue = text;
587                  _cursorBegin = cursorBegin;
588                  _keyboardRenderer.UpdateTextState(text, cursorBegin, cursorEnd, overwriteMode, null);
589  
590                  PushUpdatedState(text, cursorBegin, KeyboardResult.NotSet);
591              }
592          }
593  
594          private void HandleNpadButtonDownEvent(int npadIndex, NpadButton button)
595          {
596              lock (_lock)
597              {
598                  if (!IsKeyboardActive())
599                  {
600                      return;
601                  }
602  
603                  switch (button)
604                  {
605                      case NpadButton.A:
606                          _keyboardRenderer.UpdateCommandState(_canAcceptController, null, null);
607                          break;
608                      case NpadButton.B:
609                          _keyboardRenderer.UpdateCommandState(null, _canAcceptController, null);
610                          break;
611                  }
612              }
613          }
614  
615          private void HandleNpadButtonUpEvent(int npadIndex, NpadButton button)
616          {
617              lock (_lock)
618              {
619                  KeyboardResult result = KeyboardResult.NotSet;
620  
621                  switch (button)
622                  {
623                      case NpadButton.A:
624                          result = KeyboardResult.Accept;
625                          _keyboardRenderer.UpdateCommandState(false, null, null);
626                          break;
627                      case NpadButton.B:
628                          result = KeyboardResult.Cancel;
629                          _keyboardRenderer.UpdateCommandState(null, false, null);
630                          break;
631                  }
632  
633                  if (IsKeyboardActive())
634                  {
635                      if (!_canAcceptController)
636                      {
637                          _canAcceptController = true;
638                      }
639                      else if (InputModeControllerEnabled())
640                      {
641                          PushUpdatedState(_textValue, _cursorBegin, result);
642                      }
643                  }
644              }
645          }
646  
647          private void PushUpdatedState(string text, int cursorBegin, KeyboardResult result)
648          {
649              _lastResult = result;
650              _textValue = text;
651  
652              bool cancel = result == KeyboardResult.Cancel;
653              bool accept = result == KeyboardResult.Accept;
654  
655              if (!IsKeyboardActive())
656              {
657                  // Keyboard is not active.
658  
659                  return;
660              }
661  
662              if (accept == false && cancel == false)
663              {
664                  Logger.Debug?.Print(LogClass.ServiceAm, $"Updating keyboard text to {text} and cursor position to {cursorBegin}");
665  
666                  PushChangedString(text, (uint)cursorBegin, _backgroundState);
667              }
668              else
669              {
670                  // Disable the frontend.
671                  DeactivateFrontend();
672  
673                  // The 'Complete' state indicates the Calc request has been fulfilled by the applet.
674                  _backgroundState = InlineKeyboardState.Disappearing;
675  
676                  if (accept)
677                  {
678                      Logger.Debug?.Print(LogClass.ServiceAm, $"Sending keyboard OK with text {text}");
679  
680                      DecidedEnter(text, _backgroundState);
681                  }
682                  else if (cancel)
683                  {
684                      Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel");
685  
686                      DecidedCancel(_backgroundState);
687                  }
688  
689                  _interactiveSession.Push(InlineResponses.Default(_backgroundState));
690  
691                  Logger.Debug?.Print(LogClass.ServiceAm, $"Resetting state of the keyboard to {_backgroundState}");
692  
693                  // Set the state of the applet to 'Initialized' as it is the only known state so far
694                  // that does not soft-lock the keyboard after use.
695  
696                  _backgroundState = InlineKeyboardState.Initialized;
697  
698                  _interactiveSession.Push(InlineResponses.Default(_backgroundState));
699              }
700          }
701  
702          private void PushChangedString(string text, uint cursor, InlineKeyboardState state)
703          {
704              // TODO (Caian): The *V2 methods are not supported because the applications that request
705              // them do not seem to accept them. The regular methods seem to work just fine in all cases.
706  
707              if (_encoding == Encoding.UTF8)
708              {
709                  _interactiveSession.Push(InlineResponses.ChangedStringUtf8(text, cursor, state));
710              }
711              else
712              {
713                  _interactiveSession.Push(InlineResponses.ChangedString(text, cursor, state));
714              }
715          }
716  
717          private void DecidedEnter(string text, InlineKeyboardState state)
718          {
719              if (_encoding == Encoding.UTF8)
720              {
721                  _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(text, state));
722              }
723              else
724              {
725                  _interactiveSession.Push(InlineResponses.DecidedEnter(text, state));
726              }
727          }
728  
729          private void DecidedCancel(InlineKeyboardState state)
730          {
731              _interactiveSession.Push(InlineResponses.DecidedCancel(state));
732          }
733  
734          private void PushForegroundResponse(bool interactive)
735          {
736              int bufferSize = interactive ? InteractiveBufferSize : StandardBufferSize;
737  
738              using MemoryStream stream = new(new byte[bufferSize]);
739              using BinaryWriter writer = new(stream);
740              byte[] output = _encoding.GetBytes(_textValue);
741  
742              if (!interactive)
743              {
744                  // Result Code.
745                  writer.Write(_lastResult == KeyboardResult.Accept ? 0U : 1U);
746              }
747              else
748              {
749                  // In interactive mode, we write the length of the text as a long, rather than
750                  // a result code. This field is inclusive of the 64-bit size.
751                  writer.Write((long)output.Length + 8);
752              }
753  
754              writer.Write(output);
755  
756              if (!interactive)
757              {
758                  _normalSession.Push(stream.ToArray());
759              }
760              else
761              {
762                  _interactiveSession.Push(stream.ToArray());
763              }
764          }
765  
766          /// <summary>
767          /// Removes all Unicode control code characters from the input string.
768          /// This includes CR/LF, tabs, null characters, escape characters,
769          /// and special control codes which are used for formatting by the real keyboard applet.
770          /// </summary>
771          /// <remarks>
772          /// Some games send special control codes (such as 0x13 "Device Control 3") as part of the string.
773          /// Future implementations of the emulated keyboard applet will need to handle these as well.
774          /// </remarks>
775          /// <param name="input">The input string to sanitize (may be null).</param>
776          /// <returns>The sanitized string.</returns>
777          internal static string StripUnicodeControlCodes(string input)
778          {
779              if (input is null)
780              {
781                  return null;
782              }
783  
784              if (input.Length == 0)
785              {
786                  return string.Empty;
787              }
788  
789              StringBuilder sb = new(capacity: input.Length);
790              foreach (char c in input)
791              {
792                  if (!char.IsControl(c))
793                  {
794                      sb.Append(c);
795                  }
796              }
797  
798              return sb.ToString();
799          }
800  
801          private static T ReadStruct<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(byte[] data)
802              where T : struct
803          {
804              GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
805  
806              try
807              {
808                  return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
809              }
810              finally
811              {
812                  handle.Free();
813              }
814          }
815      }
816  }