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 }