SoftwareKeyboardApplet.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. using Ryujinx.Common.Logging;
  2. using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
  3. using Ryujinx.HLE.HOS.Services.Am.AppletAE;
  4. using System;
  5. using System.IO;
  6. using System.Runtime.InteropServices;
  7. using System.Text;
  8. using System.Threading;
  9. using System.Threading.Tasks;
  10. namespace Ryujinx.HLE.HOS.Applets
  11. {
  12. internal class SoftwareKeyboardApplet : IApplet
  13. {
  14. private const string DefaultText = "Ryujinx";
  15. private readonly Switch _device;
  16. private const int StandardBufferSize = 0x7D8;
  17. private const int InteractiveBufferSize = 0x7D4;
  18. private SoftwareKeyboardState _state = SoftwareKeyboardState.Uninitialized;
  19. private bool _isBackground = false;
  20. private AppletSession _normalSession;
  21. private AppletSession _interactiveSession;
  22. // Configuration for foreground mode
  23. private SoftwareKeyboardConfig _keyboardFgConfig;
  24. private SoftwareKeyboardCalc _keyboardCalc;
  25. private SoftwareKeyboardDictSet _keyboardDict;
  26. // Configuration for background mode
  27. private SoftwareKeyboardInitialize _keyboardBgInitialize;
  28. private byte[] _transferMemory;
  29. private string _textValue = null;
  30. private bool _okPressed = false;
  31. private Encoding _encoding = Encoding.Unicode;
  32. public event EventHandler AppletStateChanged;
  33. public SoftwareKeyboardApplet(Horizon system)
  34. {
  35. _device = system.Device;
  36. }
  37. public ResultCode Start(AppletSession normalSession,
  38. AppletSession interactiveSession)
  39. {
  40. _normalSession = normalSession;
  41. _interactiveSession = interactiveSession;
  42. _interactiveSession.DataAvailable += OnInteractiveData;
  43. var launchParams = _normalSession.Pop();
  44. var keyboardConfig = _normalSession.Pop();
  45. // TODO: A better way would be handling the background creation properly
  46. // in LibraryAppleCreator / Acessor instead of guessing by size.
  47. if (keyboardConfig.Length == Marshal.SizeOf<SoftwareKeyboardInitialize>())
  48. {
  49. _isBackground = true;
  50. _keyboardBgInitialize = ReadStruct<SoftwareKeyboardInitialize>(keyboardConfig);
  51. _state = SoftwareKeyboardState.Uninitialized;
  52. return ResultCode.Success;
  53. }
  54. else
  55. {
  56. _isBackground = false;
  57. if (keyboardConfig.Length < Marshal.SizeOf<SoftwareKeyboardConfig>())
  58. {
  59. Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf<SoftwareKeyboardConfig>():x}. Got {keyboardConfig.Length:x}");
  60. }
  61. else
  62. {
  63. _keyboardFgConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
  64. }
  65. if (!_normalSession.TryPop(out _transferMemory))
  66. {
  67. Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null");
  68. }
  69. if (_keyboardFgConfig.UseUtf8)
  70. {
  71. _encoding = Encoding.UTF8;
  72. }
  73. _state = SoftwareKeyboardState.Ready;
  74. ExecuteForegroundKeyboard();
  75. return ResultCode.Success;
  76. }
  77. }
  78. public ResultCode GetResult()
  79. {
  80. return ResultCode.Success;
  81. }
  82. private void ExecuteForegroundKeyboard()
  83. {
  84. string initialText = null;
  85. // Initial Text is always encoded as a UTF-16 string in the work buffer (passed as transfer memory)
  86. // InitialStringOffset points to the memory offset and InitialStringLength is the number of UTF-16 characters
  87. if (_transferMemory != null && _keyboardFgConfig.InitialStringLength > 0)
  88. {
  89. initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardFgConfig.InitialStringOffset, 2 * _keyboardFgConfig.InitialStringLength);
  90. }
  91. // If the max string length is 0, we set it to a large default
  92. // length.
  93. if (_keyboardFgConfig.StringLengthMax == 0)
  94. {
  95. _keyboardFgConfig.StringLengthMax = 100;
  96. }
  97. var args = new SoftwareKeyboardUiArgs
  98. {
  99. HeaderText = _keyboardFgConfig.HeaderText,
  100. SubtitleText = _keyboardFgConfig.SubtitleText,
  101. GuideText = _keyboardFgConfig.GuideText,
  102. SubmitText = (!string.IsNullOrWhiteSpace(_keyboardFgConfig.SubmitText) ? _keyboardFgConfig.SubmitText : "OK"),
  103. StringLengthMin = _keyboardFgConfig.StringLengthMin,
  104. StringLengthMax = _keyboardFgConfig.StringLengthMax,
  105. InitialText = initialText
  106. };
  107. // Call the configured GUI handler to get user's input
  108. if (_device.UiHandler == null)
  109. {
  110. Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default");
  111. _okPressed = true;
  112. }
  113. else
  114. {
  115. _okPressed = _device.UiHandler.DisplayInputDialog(args, out _textValue);
  116. }
  117. _textValue ??= initialText ?? DefaultText;
  118. // If the game requests a string with a minimum length less
  119. // than our default text, repeat our default text until we meet
  120. // the minimum length requirement.
  121. // This should always be done before the text truncation step.
  122. while (_textValue.Length < _keyboardFgConfig.StringLengthMin)
  123. {
  124. _textValue = String.Join(" ", _textValue, _textValue);
  125. }
  126. // If our default text is longer than the allowed length,
  127. // we truncate it.
  128. if (_textValue.Length > _keyboardFgConfig.StringLengthMax)
  129. {
  130. _textValue = _textValue.Substring(0, (int)_keyboardFgConfig.StringLengthMax);
  131. }
  132. // Does the application want to validate the text itself?
  133. if (_keyboardFgConfig.CheckText)
  134. {
  135. // The application needs to validate the response, so we
  136. // submit it to the interactive output buffer, and poll it
  137. // for validation. Once validated, the application will submit
  138. // back a validation status, which is handled in OnInteractiveDataPushIn.
  139. _state = SoftwareKeyboardState.ValidationPending;
  140. _interactiveSession.Push(BuildResponse(_textValue, true));
  141. }
  142. else
  143. {
  144. // If the application doesn't need to validate the response,
  145. // we push the data to the non-interactive output buffer
  146. // and poll it for completion.
  147. _state = SoftwareKeyboardState.Complete;
  148. _normalSession.Push(BuildResponse(_textValue, false));
  149. AppletStateChanged?.Invoke(this, null);
  150. }
  151. }
  152. private void OnInteractiveData(object sender, EventArgs e)
  153. {
  154. // Obtain the validation status response.
  155. var data = _interactiveSession.Pop();
  156. if (_isBackground)
  157. {
  158. OnBackgroundInteractiveData(data);
  159. }
  160. else
  161. {
  162. OnForegroundInteractiveData(data);
  163. }
  164. }
  165. private void OnForegroundInteractiveData(byte[] data)
  166. {
  167. if (_state == SoftwareKeyboardState.ValidationPending)
  168. {
  169. // TODO(jduncantor):
  170. // If application rejects our "attempt", submit another attempt,
  171. // and put the applet back in PendingValidation state.
  172. // For now we assume success, so we push the final result
  173. // to the standard output buffer and carry on our merry way.
  174. _normalSession.Push(BuildResponse(_textValue, false));
  175. AppletStateChanged?.Invoke(this, null);
  176. _state = SoftwareKeyboardState.Complete;
  177. }
  178. else if(_state == SoftwareKeyboardState.Complete)
  179. {
  180. // If we have already completed, we push the result text
  181. // back on the output buffer and poll the application.
  182. _normalSession.Push(BuildResponse(_textValue, false));
  183. AppletStateChanged?.Invoke(this, null);
  184. }
  185. else
  186. {
  187. // We shouldn't be able to get here through standard swkbd execution.
  188. throw new InvalidOperationException("Software Keyboard is in an invalid state.");
  189. }
  190. }
  191. private void OnBackgroundInteractiveData(byte[] data)
  192. {
  193. // WARNING: Only invoke applet state changes after an explicit finalization
  194. // request from the game, this is because the inline keyboard is expected to
  195. // keep running in the background sending data by itself.
  196. using (MemoryStream stream = new MemoryStream(data))
  197. using (BinaryReader reader = new BinaryReader(stream))
  198. {
  199. var request = (InlineKeyboardRequest)reader.ReadUInt32();
  200. long remaining;
  201. // Always show the keyboard if the state is 'Ready'.
  202. bool showKeyboard = _state == SoftwareKeyboardState.Ready;
  203. switch (request)
  204. {
  205. case InlineKeyboardRequest.Unknown0: // Unknown request sent by some games after calc
  206. _interactiveSession.Push(InlineResponses.Default());
  207. break;
  208. case InlineKeyboardRequest.UseChangedStringV2:
  209. // Not used because we only send the entire string after confirmation.
  210. _interactiveSession.Push(InlineResponses.Default());
  211. break;
  212. case InlineKeyboardRequest.UseMovedCursorV2:
  213. // Not used because we only send the entire string after confirmation.
  214. _interactiveSession.Push(InlineResponses.Default());
  215. break;
  216. case InlineKeyboardRequest.SetCustomizeDic:
  217. remaining = stream.Length - stream.Position;
  218. if (remaining != Marshal.SizeOf<SoftwareKeyboardDictSet>())
  219. {
  220. Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes!");
  221. }
  222. else
  223. {
  224. var keyboardDictData = reader.ReadBytes((int)remaining);
  225. _keyboardDict = ReadStruct<SoftwareKeyboardDictSet>(keyboardDictData);
  226. }
  227. _interactiveSession.Push(InlineResponses.Default());
  228. break;
  229. case InlineKeyboardRequest.Calc:
  230. // Put the keyboard in a Ready state, this will force showing
  231. _state = SoftwareKeyboardState.Ready;
  232. remaining = stream.Length - stream.Position;
  233. if (remaining != Marshal.SizeOf<SoftwareKeyboardCalc>())
  234. {
  235. Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes!");
  236. }
  237. else
  238. {
  239. var keyboardCalcData = reader.ReadBytes((int)remaining);
  240. _keyboardCalc = ReadStruct<SoftwareKeyboardCalc>(keyboardCalcData);
  241. if (_keyboardCalc.Utf8Mode == 0x1)
  242. {
  243. _encoding = Encoding.UTF8;
  244. }
  245. // Force showing the keyboard regardless of the state, an unwanted
  246. // input dialog may show, but it is better than a soft lock.
  247. if (_keyboardCalc.Appear.ShouldBeHidden == 0)
  248. {
  249. showKeyboard = true;
  250. }
  251. }
  252. // Send an initialization finished signal.
  253. _interactiveSession.Push(InlineResponses.FinishedInitialize());
  254. // Start a task with the GUI handler to get user's input.
  255. new Task(() =>
  256. {
  257. bool submit = true;
  258. string inputText = (!string.IsNullOrWhiteSpace(_keyboardCalc.InputText) ? _keyboardCalc.InputText : DefaultText);
  259. // Call the configured GUI handler to get user's input.
  260. if (!showKeyboard)
  261. {
  262. // Submit the default text to avoid soft locking if the keyboard was ignored by
  263. // accident. It's better to change the name than being locked out of the game.
  264. submit = true;
  265. inputText = DefaultText;
  266. Logger.Debug?.Print(LogClass.Application, "Received a dummy Calc, keyboard will not be shown");
  267. }
  268. else if (_device.UiHandler == null)
  269. {
  270. Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default");
  271. }
  272. else
  273. {
  274. var args = new SoftwareKeyboardUiArgs
  275. {
  276. HeaderText = "", // The inline keyboard lacks these texts
  277. SubtitleText = "",
  278. GuideText = "",
  279. SubmitText = (!string.IsNullOrWhiteSpace(_keyboardCalc.Appear.OkText) ? _keyboardCalc.Appear.OkText : "OK"),
  280. StringLengthMin = 0,
  281. StringLengthMax = 100,
  282. InitialText = inputText
  283. };
  284. submit = _device.UiHandler.DisplayInputDialog(args, out inputText);
  285. }
  286. if (submit)
  287. {
  288. Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard OK...");
  289. if (_encoding == Encoding.UTF8)
  290. {
  291. _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(inputText));
  292. }
  293. else
  294. {
  295. _interactiveSession.Push(InlineResponses.DecidedEnter(inputText));
  296. }
  297. }
  298. else
  299. {
  300. Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel...");
  301. _interactiveSession.Push(InlineResponses.DecidedCancel());
  302. }
  303. // TODO: Why is this necessary? Does the software expect a constant stream of responses?
  304. Thread.Sleep(500);
  305. Logger.Debug?.Print(LogClass.ServiceAm, "Resetting state of the keyboard...");
  306. _interactiveSession.Push(InlineResponses.Default());
  307. }).Start();
  308. break;
  309. case InlineKeyboardRequest.Finalize:
  310. // The game wants to close the keyboard applet and will wait for a state change.
  311. _state = SoftwareKeyboardState.Uninitialized;
  312. AppletStateChanged?.Invoke(this, null);
  313. break;
  314. default:
  315. // We shouldn't be able to get here through standard swkbd execution.
  316. Logger.Error?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_state}!");
  317. _interactiveSession.Push(InlineResponses.Default());
  318. break;
  319. }
  320. }
  321. }
  322. private byte[] BuildResponse(string text, bool interactive)
  323. {
  324. int bufferSize = interactive ? InteractiveBufferSize : StandardBufferSize;
  325. using (MemoryStream stream = new MemoryStream(new byte[bufferSize]))
  326. using (BinaryWriter writer = new BinaryWriter(stream))
  327. {
  328. byte[] output = _encoding.GetBytes(text);
  329. if (!interactive)
  330. {
  331. // Result Code
  332. writer.Write(_okPressed ? 0U : 1U);
  333. }
  334. else
  335. {
  336. // In interactive mode, we write the length of the text as a long, rather than
  337. // a result code. This field is inclusive of the 64-bit size.
  338. writer.Write((long)output.Length + 8);
  339. }
  340. writer.Write(output);
  341. return stream.ToArray();
  342. }
  343. }
  344. private static T ReadStruct<T>(byte[] data)
  345. where T : struct
  346. {
  347. GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
  348. try
  349. {
  350. return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
  351. }
  352. finally
  353. {
  354. handle.Free();
  355. }
  356. }
  357. }
  358. }