diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.HLE/HOS/Applets | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.HLE/HOS/Applets')
69 files changed, 4807 insertions, 0 deletions
diff --git a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs new file mode 100644 index 00000000..a686a832 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs @@ -0,0 +1,37 @@ +using Ryujinx.HLE.HOS.Applets.Browser; +using Ryujinx.HLE.HOS.Applets.Error; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using System; +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Applets +{ + static class AppletManager + { + private static Dictionary<AppletId, Type> _appletMapping; + + static AppletManager() + { + _appletMapping = new Dictionary<AppletId, Type> + { + { AppletId.Error, typeof(ErrorApplet) }, + { AppletId.PlayerSelect, typeof(PlayerSelectApplet) }, + { AppletId.Controller, typeof(ControllerApplet) }, + { AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) }, + { AppletId.LibAppletWeb, typeof(BrowserApplet) }, + { AppletId.LibAppletShop, typeof(BrowserApplet) }, + { AppletId.LibAppletOff, typeof(BrowserApplet) } + }; + } + + public static IApplet Create(AppletId applet, Horizon system) + { + if (_appletMapping.TryGetValue(applet, out Type appletClass)) + { + return (IApplet)Activator.CreateInstance(appletClass, system); + } + + throw new NotImplementedException($"{applet} applet is not implemented."); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/BootDisplayKind.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/BootDisplayKind.cs new file mode 100644 index 00000000..fe6e6040 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/BootDisplayKind.cs @@ -0,0 +1,11 @@ +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + enum BootDisplayKind + { + White, + Offline, + Black, + Share, + Lobby + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs new file mode 100644 index 00000000..952afcd5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs @@ -0,0 +1,104 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + internal class BrowserApplet : IApplet + { + public event EventHandler AppletStateChanged; + + private AppletSession _normalSession; + private AppletSession _interactiveSession; + + private CommonArguments _commonArguments; + private List<BrowserArgument> _arguments; + private ShimKind _shimKind; + + public BrowserApplet(Horizon system) {} + + public ResultCode GetResult() + { + return ResultCode.Success; + } + + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) + { + _normalSession = normalSession; + _interactiveSession = interactiveSession; + + _commonArguments = IApplet.ReadStruct<CommonArguments>(_normalSession.Pop()); + + Logger.Stub?.PrintStub(LogClass.ServiceAm, $"WebApplet version: 0x{_commonArguments.AppletVersion:x8}"); + + ReadOnlySpan<byte> webArguments = _normalSession.Pop(); + + (_shimKind, _arguments) = BrowserArgument.ParseArguments(webArguments); + + Logger.Stub?.PrintStub(LogClass.ServiceAm, $"Web Arguments: {_arguments.Count}"); + + foreach (BrowserArgument argument in _arguments) + { + Logger.Stub?.PrintStub(LogClass.ServiceAm, $"{argument.Type}: {argument.GetValue()}"); + } + + if ((_commonArguments.AppletVersion >= 0x80000 && _shimKind == ShimKind.Web) || (_commonArguments.AppletVersion >= 0x30000 && _shimKind == ShimKind.Share)) + { + List<BrowserOutput> result = new List<BrowserOutput>(); + + result.Add(new BrowserOutput(BrowserOutputType.ExitReason, (uint)WebExitReason.ExitButton)); + + _normalSession.Push(BuildResponseNew(result)); + } + else + { + WebCommonReturnValue result = new WebCommonReturnValue() + { + ExitReason = WebExitReason.ExitButton, + }; + + _normalSession.Push(BuildResponseOld(result)); + } + + AppletStateChanged?.Invoke(this, null); + + return ResultCode.Success; + } + + private byte[] BuildResponseOld(WebCommonReturnValue result) + { + using (MemoryStream stream = MemoryStreamManager.Shared.GetStream()) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.WriteStruct(result); + + return stream.ToArray(); + } + } + private byte[] BuildResponseNew(List<BrowserOutput> outputArguments) + { + using (MemoryStream stream = MemoryStreamManager.Shared.GetStream()) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.WriteStruct(new WebArgHeader + { + Count = (ushort)outputArguments.Count, + ShimKind = _shimKind + }); + + foreach (BrowserOutput output in outputArguments) + { + output.Write(writer); + } + + writer.Write(new byte[0x2000 - writer.BaseStream.Position]); + + return stream.ToArray(); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserArgument.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserArgument.cs new file mode 100644 index 00000000..17fd4089 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserArgument.cs @@ -0,0 +1,133 @@ +using Ryujinx.HLE.HOS.Services.Account.Acc; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + class BrowserArgument + { + public WebArgTLVType Type { get; } + public byte[] Value { get; } + + public BrowserArgument(WebArgTLVType type, byte[] value) + { + Type = type; + Value = value; + } + + private static readonly Dictionary<WebArgTLVType, Type> _typeRegistry = new Dictionary<WebArgTLVType, Type> + { + { WebArgTLVType.InitialURL, typeof(string) }, + { WebArgTLVType.CallbackUrl, typeof(string) }, + { WebArgTLVType.CallbackableUrl, typeof(string) }, + { WebArgTLVType.ApplicationId, typeof(ulong) }, + { WebArgTLVType.DocumentPath, typeof(string) }, + { WebArgTLVType.DocumentKind, typeof(DocumentKind) }, + { WebArgTLVType.SystemDataId, typeof(ulong) }, + { WebArgTLVType.Whitelist, typeof(string) }, + { WebArgTLVType.NewsFlag, typeof(bool) }, + { WebArgTLVType.UserID, typeof(UserId) }, + { WebArgTLVType.ScreenShotEnabled, typeof(bool) }, + { WebArgTLVType.EcClientCertEnabled, typeof(bool) }, + { WebArgTLVType.UnknownFlag0x14, typeof(bool) }, + { WebArgTLVType.UnknownFlag0x15, typeof(bool) }, + { WebArgTLVType.PlayReportEnabled, typeof(bool) }, + { WebArgTLVType.BootDisplayKind, typeof(BootDisplayKind) }, + { WebArgTLVType.FooterEnabled, typeof(bool) }, + { WebArgTLVType.PointerEnabled, typeof(bool) }, + { WebArgTLVType.LeftStickMode, typeof(LeftStickMode) }, + { WebArgTLVType.KeyRepeatFrame1, typeof(int) }, + { WebArgTLVType.KeyRepeatFrame2, typeof(int) }, + { WebArgTLVType.BootAsMediaPlayerInverted, typeof(bool) }, + { WebArgTLVType.DisplayUrlKind, typeof(bool) }, + { WebArgTLVType.BootAsMediaPlayer, typeof(bool) }, + { WebArgTLVType.ShopJumpEnabled, typeof(bool) }, + { WebArgTLVType.MediaAutoPlayEnabled, typeof(bool) }, + { WebArgTLVType.LobbyParameter, typeof(string) }, + { WebArgTLVType.JsExtensionEnabled, typeof(bool) }, + { WebArgTLVType.AdditionalCommentText, typeof(string) }, + { WebArgTLVType.TouchEnabledOnContents, typeof(bool) }, + { WebArgTLVType.UserAgentAdditionalString, typeof(string) }, + { WebArgTLVType.MediaPlayerAutoCloseEnabled, typeof(bool) }, + { WebArgTLVType.PageCacheEnabled, typeof(bool) }, + { WebArgTLVType.WebAudioEnabled, typeof(bool) }, + { WebArgTLVType.PageFadeEnabled, typeof(bool) }, + { WebArgTLVType.BootLoadingIconEnabled, typeof(bool) }, + { WebArgTLVType.PageScrollIndicatorEnabled, typeof(bool) }, + { WebArgTLVType.MediaPlayerSpeedControlEnabled, typeof(bool) }, + { WebArgTLVType.OverrideWebAudioVolume, typeof(float) }, + { WebArgTLVType.OverrideMediaAudioVolume, typeof(float) }, + { WebArgTLVType.MediaPlayerUiEnabled, typeof(bool) }, + }; + + public static (ShimKind, List<BrowserArgument>) ParseArguments(ReadOnlySpan<byte> data) + { + List<BrowserArgument> browserArguments = new List<BrowserArgument>(); + + WebArgHeader header = IApplet.ReadStruct<WebArgHeader>(data.Slice(0, 8)); + + ReadOnlySpan<byte> rawTLVs = data.Slice(8); + + for (int i = 0; i < header.Count; i++) + { + WebArgTLV tlv = IApplet.ReadStruct<WebArgTLV>(rawTLVs); + ReadOnlySpan<byte> tlvData = rawTLVs.Slice(Unsafe.SizeOf<WebArgTLV>(), tlv.Size); + + browserArguments.Add(new BrowserArgument((WebArgTLVType)tlv.Type, tlvData.ToArray())); + + rawTLVs = rawTLVs.Slice(Unsafe.SizeOf<WebArgTLV>() + tlv.Size); + } + + return (header.ShimKind, browserArguments); + } + + public object GetValue() + { + if (_typeRegistry.TryGetValue(Type, out Type valueType)) + { + if (valueType == typeof(string)) + { + return Encoding.UTF8.GetString(Value); + } + else if (valueType == typeof(bool)) + { + return Value[0] == 1; + } + else if (valueType == typeof(uint)) + { + return BitConverter.ToUInt32(Value); + } + else if (valueType == typeof(int)) + { + return BitConverter.ToInt32(Value); + } + else if (valueType == typeof(ulong)) + { + return BitConverter.ToUInt64(Value); + } + else if (valueType == typeof(long)) + { + return BitConverter.ToInt64(Value); + } + else if (valueType == typeof(float)) + { + return BitConverter.ToSingle(Value); + } + else if (valueType == typeof(UserId)) + { + return new UserId(Value); + } + else if (valueType.IsEnum) + { + return Enum.ToObject(valueType, BitConverter.ToInt32(Value)); + } + + return $"{valueType.Name} parsing not implemented"; + } + + return $"Unknown value format (raw length: {Value.Length})"; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserOutput.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserOutput.cs new file mode 100644 index 00000000..0b368262 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserOutput.cs @@ -0,0 +1,47 @@ +using Ryujinx.Common; +using System; +using System.IO; + +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + class BrowserOutput + { + public BrowserOutputType Type { get; } + public byte[] Value { get; } + + public BrowserOutput(BrowserOutputType type, byte[] value) + { + Type = type; + Value = value; + } + + public BrowserOutput(BrowserOutputType type, uint value) + { + Type = type; + Value = BitConverter.GetBytes(value); + } + + public BrowserOutput(BrowserOutputType type, ulong value) + { + Type = type; + Value = BitConverter.GetBytes(value); + } + + public BrowserOutput(BrowserOutputType type, bool value) + { + Type = type; + Value = BitConverter.GetBytes(value); + } + + public void Write(BinaryWriter writer) + { + writer.WriteStruct(new WebArgTLV + { + Type = (ushort)Type, + Size = (ushort)Value.Length + }); + + writer.Write(Value); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserOutputType.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserOutputType.cs new file mode 100644 index 00000000..209ae8ae --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserOutputType.cs @@ -0,0 +1,14 @@ +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + enum BrowserOutputType : ushort + { + ExitReason = 0x1, + LastUrl = 0x2, + LastUrlSize = 0x3, + SharePostResult = 0x4, + PostServiceName = 0x5, + PostServiceNameSize = 0x6, + PostId = 0x7, + MediaPlayerAutoClosedByCompletion = 0x8 + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/DocumentKind.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/DocumentKind.cs new file mode 100644 index 00000000..385bcdd0 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/DocumentKind.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + enum DocumentKind + { + OfflineHtmlPage = 1, + ApplicationLegalInformation, + SystemDataPage + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/LeftStickMode.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/LeftStickMode.cs new file mode 100644 index 00000000..917549d2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/LeftStickMode.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + enum LeftStickMode + { + Pointer = 0, + Cursor + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/ShimKind.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/ShimKind.cs new file mode 100644 index 00000000..ca2ef32f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/ShimKind.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + public enum ShimKind : uint + { + Shop = 1, + Login, + Offline, + Share, + Web, + Wifi, + Lobby + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/WebArgHeader.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/WebArgHeader.cs new file mode 100644 index 00000000..c5e19f6c --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/WebArgHeader.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + public struct WebArgHeader + { + public ushort Count; + public ushort Padding; + public ShimKind ShimKind; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/WebArgTLV.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/WebArgTLV.cs new file mode 100644 index 00000000..f6c1e5ae --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/WebArgTLV.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + public struct WebArgTLV + { + public ushort Type; + public ushort Size; + public uint Padding; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/WebArgTLVType.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/WebArgTLVType.cs new file mode 100644 index 00000000..bd303207 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/WebArgTLVType.cs @@ -0,0 +1,62 @@ +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + enum WebArgTLVType : ushort + { + InitialURL = 0x1, + CallbackUrl = 0x3, + CallbackableUrl = 0x4, + ApplicationId = 0x5, + DocumentPath = 0x6, + DocumentKind = 0x7, + SystemDataId = 0x8, + ShareStartPage = 0x9, + Whitelist = 0xA, + NewsFlag = 0xB, + UserID = 0xE, + AlbumEntry0 = 0xF, + ScreenShotEnabled = 0x10, + EcClientCertEnabled = 0x11, + PlayReportEnabled = 0x13, + UnknownFlag0x14 = 0x14, + UnknownFlag0x15 = 0x15, + BootDisplayKind = 0x17, + BackgroundKind = 0x18, + FooterEnabled = 0x19, + PointerEnabled = 0x1A, + LeftStickMode = 0x1B, + KeyRepeatFrame1 = 0x1C, + KeyRepeatFrame2 = 0x1D, + BootAsMediaPlayerInverted = 0x1E, + DisplayUrlKind = 0x1F, + BootAsMediaPlayer = 0x21, + ShopJumpEnabled = 0x22, + MediaAutoPlayEnabled = 0x23, + LobbyParameter = 0x24, + ApplicationAlbumEntry = 0x26, + JsExtensionEnabled = 0x27, + AdditionalCommentText = 0x28, + TouchEnabledOnContents = 0x29, + UserAgentAdditionalString = 0x2A, + AdditionalMediaData0 = 0x2B, + MediaPlayerAutoCloseEnabled = 0x2C, + PageCacheEnabled = 0x2D, + WebAudioEnabled = 0x2E, + FooterFixedKind = 0x32, + PageFadeEnabled = 0x33, + MediaCreatorApplicationRatingAge = 0x34, + BootLoadingIconEnabled = 0x35, + PageScrollIndicatorEnabled = 0x36, + MediaPlayerSpeedControlEnabled = 0x37, + AlbumEntry1 = 0x38, + AlbumEntry2 = 0x39, + AlbumEntry3 = 0x3A, + AdditionalMediaData1 = 0x3B, + AdditionalMediaData2 = 0x3C, + AdditionalMediaData3 = 0x3D, + BootFooterButton = 0x3E, + OverrideWebAudioVolume = 0x3F, + OverrideMediaAudioVolume = 0x40, + BootMode = 0x41, + MediaPlayerUiEnabled = 0x43 + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/WebCommonReturnValue.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/WebCommonReturnValue.cs new file mode 100644 index 00000000..9f7eae70 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/WebCommonReturnValue.cs @@ -0,0 +1,12 @@ +using Ryujinx.Common.Memory; + +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + public struct WebCommonReturnValue + { + public WebExitReason ExitReason; + public uint Padding; + public ByteArray4096 LastUrl; + public ulong LastUrlSize; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/WebExitReason.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/WebExitReason.cs new file mode 100644 index 00000000..4e44d34a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/WebExitReason.cs @@ -0,0 +1,11 @@ +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + public enum WebExitReason : uint + { + ExitButton, + BackButton, + Requested, + LastUrl, + ErrorDialog = 7 + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/CommonArguments.cs b/src/Ryujinx.HLE/HOS/Applets/CommonArguments.cs new file mode 100644 index 00000000..5da34db1 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/CommonArguments.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets +{ + [StructLayout(LayoutKind.Sequential, Pack = 8)] + struct CommonArguments + { + public uint Version; + public uint StructureSize; + public uint AppletVersion; + public uint ThemeColor; + [MarshalAs(UnmanagedType.I1)] + public bool PlayStartupSound; + public ulong SystemTicks; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs new file mode 100644 index 00000000..5d5a26c2 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs @@ -0,0 +1,147 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using Ryujinx.HLE.HOS.Services.Hid; +using Ryujinx.HLE.HOS.Services.Hid.Types; +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using static Ryujinx.HLE.HOS.Services.Hid.HidServer.HidUtils; + +namespace Ryujinx.HLE.HOS.Applets +{ + internal class ControllerApplet : IApplet + { + private Horizon _system; + + private AppletSession _normalSession; + + public event EventHandler AppletStateChanged; + + public ControllerApplet(Horizon system) + { + _system = system; + } + + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) + { + _normalSession = normalSession; + + byte[] launchParams = _normalSession.Pop(); + byte[] controllerSupportArgPrivate = _normalSession.Pop(); + ControllerSupportArgPrivate privateArg = IApplet.ReadStruct<ControllerSupportArgPrivate>(controllerSupportArgPrivate); + + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet ArgPriv {privateArg.PrivateSize} {privateArg.ArgSize} {privateArg.Mode} " + + $"HoldType:{(NpadJoyHoldType)privateArg.NpadJoyHoldType} StyleSets:{(ControllerType)privateArg.NpadStyleSet}"); + + if (privateArg.Mode != ControllerSupportMode.ShowControllerSupport) + { + _normalSession.Push(BuildResponse()); // Dummy response for other modes + AppletStateChanged?.Invoke(this, null); + + return ResultCode.Success; + } + + byte[] controllerSupportArg = _normalSession.Pop(); + + ControllerSupportArgHeader argHeader; + + if (privateArg.ArgSize == Marshal.SizeOf<ControllerSupportArgV7>()) + { + ControllerSupportArgV7 arg = IApplet.ReadStruct<ControllerSupportArgV7>(controllerSupportArg); + argHeader = arg.Header; + + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version 7 EnableExplainText={arg.EnableExplainText != 0}"); + // Read enable text here? + } + else if (privateArg.ArgSize == Marshal.SizeOf<ControllerSupportArgVPre7>()) + { + ControllerSupportArgVPre7 arg = IApplet.ReadStruct<ControllerSupportArgVPre7>(controllerSupportArg); + argHeader = arg.Header; + + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version Pre-7 EnableExplainText={arg.EnableExplainText != 0}"); + // Read enable text here? + } + else + { + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version Unknown"); + + argHeader = IApplet.ReadStruct<ControllerSupportArgHeader>(controllerSupportArg); // Read just the header + } + + int playerMin = argHeader.PlayerCountMin; + int playerMax = argHeader.PlayerCountMax; + bool singleMode = argHeader.EnableSingleMode != 0; + + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet Arg {playerMin} {playerMax} {argHeader.EnableTakeOverConnection} {argHeader.EnableSingleMode}"); + + if (singleMode) + { + // Applications can set an arbitrary player range even with SingleMode, so clamp it + playerMin = playerMax = 1; + } + + int configuredCount = 0; + PlayerIndex primaryIndex = PlayerIndex.Unknown; + while (!_system.Device.Hid.Npads.Validate(playerMin, playerMax, (ControllerType)privateArg.NpadStyleSet, out configuredCount, out primaryIndex)) + { + ControllerAppletUiArgs uiArgs = new ControllerAppletUiArgs + { + PlayerCountMin = playerMin, + PlayerCountMax = playerMax, + SupportedStyles = (ControllerType)privateArg.NpadStyleSet, + SupportedPlayers = _system.Device.Hid.Npads.GetSupportedPlayers(), + IsDocked = _system.State.DockedMode + }; + + if (!_system.Device.UiHandler.DisplayMessageDialog(uiArgs)) + { + break; + } + } + + ControllerSupportResultInfo result = new ControllerSupportResultInfo + { + PlayerCount = (sbyte)configuredCount, + SelectedId = (uint)GetNpadIdTypeFromIndex(primaryIndex) + }; + + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet ReturnResult {result.PlayerCount} {result.SelectedId}"); + + _normalSession.Push(BuildResponse(result)); + AppletStateChanged?.Invoke(this, null); + + _system.ReturnFocus(); + + return ResultCode.Success; + } + + public ResultCode GetResult() + { + return ResultCode.Success; + } + + private byte[] BuildResponse(ControllerSupportResultInfo result) + { + using (MemoryStream stream = MemoryStreamManager.Shared.GetStream()) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(ref result, Unsafe.SizeOf<ControllerSupportResultInfo>()))); + + return stream.ToArray(); + } + } + + private byte[] BuildResponse() + { + using (MemoryStream stream = MemoryStreamManager.Shared.GetStream()) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.Write((ulong)ResultCode.Success); + + return stream.ToArray(); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerAppletUiArgs.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerAppletUiArgs.cs new file mode 100644 index 00000000..cc15a406 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerAppletUiArgs.cs @@ -0,0 +1,14 @@ +using Ryujinx.HLE.HOS.Services.Hid; +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Applets +{ + public struct ControllerAppletUiArgs + { + public int PlayerCountMin; + public int PlayerCountMax; + public ControllerType SupportedStyles; + public IEnumerable<PlayerIndex> SupportedPlayers; + public bool IsDocked; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgHeader.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgHeader.cs new file mode 100644 index 00000000..141994a8 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgHeader.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets +{ +#pragma warning disable CS0649 + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ControllerSupportArgHeader + { + public sbyte PlayerCountMin; + public sbyte PlayerCountMax; + public byte EnableTakeOverConnection; + public byte EnableLeftJustify; + public byte EnablePermitJoyDual; + public byte EnableSingleMode; + public byte EnableIdentificationColor; + } +#pragma warning restore CS0649 +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgPrivate.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgPrivate.cs new file mode 100644 index 00000000..d4c8177e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgPrivate.cs @@ -0,0 +1,16 @@ +namespace Ryujinx.HLE.HOS.Applets +{ +#pragma warning disable CS0649 + struct ControllerSupportArgPrivate + { + public uint PrivateSize; + public uint ArgSize; + public byte Flag0; + public byte Flag1; + public ControllerSupportMode Mode; + public byte ControllerSupportCaller; + public uint NpadStyleSet; + public uint NpadJoyHoldType; + } +#pragma warning restore CS0649 +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgV7.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgV7.cs new file mode 100644 index 00000000..98c413be --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgV7.cs @@ -0,0 +1,26 @@ +using Ryujinx.Common.Memory; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets +{ +#pragma warning disable CS0649 + // (8.0.0+ version) + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ControllerSupportArgV7 + { + public ControllerSupportArgHeader Header; + public Array8<uint> IdentificationColor; + public byte EnableExplainText; + public ExplainTextStruct ExplainText; + + [StructLayout(LayoutKind.Sequential, Size = 8 * 0x81)] + public struct ExplainTextStruct + { + private byte element; + + public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref element, 8 * 0x81); + } + } +#pragma warning restore CS0649 +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgVPre7.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgVPre7.cs new file mode 100644 index 00000000..87417e16 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgVPre7.cs @@ -0,0 +1,26 @@ +using Ryujinx.Common.Memory; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets +{ +#pragma warning disable CS0649 + // (1.0.0+ version) + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ControllerSupportArgVPre7 + { + public ControllerSupportArgHeader Header; + public Array4<uint> IdentificationColor; + public byte EnableExplainText; + public ExplainTextStruct ExplainText; + + [StructLayout(LayoutKind.Sequential, Size = 4 * 0x81)] + public struct ExplainTextStruct + { + private byte element; + + public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref element, 4 * 0x81); + } + } +#pragma warning restore CS0649 +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportMode.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportMode.cs new file mode 100644 index 00000000..9496c1dd --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportMode.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Applets +{ + enum ControllerSupportMode : byte + { + ShowControllerSupport = 0, + ShowControllerStrapGuide = 1, + ShowControllerFirmwareUpdate = 2 + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportResultInfo.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportResultInfo.cs new file mode 100644 index 00000000..689a54de --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportResultInfo.cs @@ -0,0 +1,16 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets +{ +#pragma warning disable CS0649 + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ControllerSupportResultInfo + { + public sbyte PlayerCount; + private Array3<byte> _padding; + public uint SelectedId; + public uint Result; + } +#pragma warning restore CS0649 +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ApplicationErrorArg.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ApplicationErrorArg.cs new file mode 100644 index 00000000..f40d5411 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Error/ApplicationErrorArg.cs @@ -0,0 +1,14 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.Error +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ApplicationErrorArg + { + public uint ErrorNumber; + public ulong LanguageCode; + public ByteArray2048 MessageText; + public ByteArray2048 DetailsText; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs new file mode 100644 index 00000000..c5c6e8e9 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs @@ -0,0 +1,216 @@ +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using Ryujinx.HLE.HOS.SystemState; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; + +namespace Ryujinx.HLE.HOS.Applets.Error +{ + internal partial class ErrorApplet : IApplet + { + private const long ErrorMessageBinaryTitleId = 0x0100000000000801; + + private Horizon _horizon; + private AppletSession _normalSession; + private CommonArguments _commonArguments; + private ErrorCommonHeader _errorCommonHeader; + private byte[] _errorStorage; + + public event EventHandler AppletStateChanged; + + [GeneratedRegex(@"[^\u0000\u0009\u000A\u000D\u0020-\uFFFF]..")] + private static partial Regex CleanTextRegex(); + + public ErrorApplet(Horizon horizon) + { + _horizon = horizon; + } + + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) + { + _normalSession = normalSession; + _commonArguments = IApplet.ReadStruct<CommonArguments>(_normalSession.Pop()); + + Logger.Info?.PrintMsg(LogClass.ServiceAm, $"ErrorApplet version: 0x{_commonArguments.AppletVersion:x8}"); + + _errorStorage = _normalSession.Pop(); + _errorCommonHeader = IApplet.ReadStruct<ErrorCommonHeader>(_errorStorage); + _errorStorage = _errorStorage.Skip(Marshal.SizeOf<ErrorCommonHeader>()).ToArray(); + + switch (_errorCommonHeader.Type) + { + case ErrorType.ErrorCommonArg: + { + ParseErrorCommonArg(); + + break; + } + case ErrorType.ApplicationErrorArg: + { + ParseApplicationErrorArg(); + + break; + } + default: throw new NotImplementedException($"ErrorApplet type {_errorCommonHeader.Type} is not implemented."); + } + + AppletStateChanged?.Invoke(this, null); + + return ResultCode.Success; + } + + private (uint module, uint description) HexToResultCode(uint resultCode) + { + return ((resultCode & 0x1FF) + 2000, (resultCode >> 9) & 0x3FFF); + } + + private string SystemLanguageToLanguageKey(SystemLanguage systemLanguage) + { + return systemLanguage switch + { + SystemLanguage.Japanese => "ja", + SystemLanguage.AmericanEnglish => "en-US", + SystemLanguage.French => "fr", + SystemLanguage.German => "de", + SystemLanguage.Italian => "it", + SystemLanguage.Spanish => "es", + SystemLanguage.Chinese => "zh-Hans", + SystemLanguage.Korean => "ko", + SystemLanguage.Dutch => "nl", + SystemLanguage.Portuguese => "pt", + SystemLanguage.Russian => "ru", + SystemLanguage.Taiwanese => "zh-HansT", + SystemLanguage.BritishEnglish => "en-GB", + SystemLanguage.CanadianFrench => "fr-CA", + SystemLanguage.LatinAmericanSpanish => "es-419", + SystemLanguage.SimplifiedChinese => "zh-Hans", + SystemLanguage.TraditionalChinese => "zh-Hant", + SystemLanguage.BrazilianPortuguese => "pt-BR", + _ => "en-US" + }; + } + + private static string CleanText(string value) + { + return CleanTextRegex().Replace(value, "").Replace("\0", ""); + } + + private string GetMessageText(uint module, uint description, string key) + { + string binaryTitleContentPath = _horizon.ContentManager.GetInstalledContentPath(ErrorMessageBinaryTitleId, StorageId.BuiltInSystem, NcaContentType.Data); + + using (LibHac.Fs.IStorage ncaFileStream = new LocalStorage(_horizon.Device.FileSystem.SwitchPathToSystemPath(binaryTitleContentPath), FileAccess.Read, FileMode.Open)) + { + Nca nca = new Nca(_horizon.Device.FileSystem.KeySet, ncaFileStream); + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, _horizon.FsIntegrityCheckLevel); + string languageCode = SystemLanguageToLanguageKey(_horizon.State.DesiredSystemLanguage); + string filePath = $"/{module}/{description:0000}/{languageCode}_{key}"; + + if (romfs.FileExists(filePath)) + { + using var binaryFile = new UniqueRef<IFile>(); + + romfs.OpenFile(ref binaryFile.Ref, filePath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + StreamReader reader = new StreamReader(binaryFile.Get.AsStream(), Encoding.Unicode); + + return CleanText(reader.ReadToEnd()); + } + else + { + return ""; + } + } + } + + private string[] GetButtonsText(uint module, uint description, string key) + { + string buttonsText = GetMessageText(module, description, key); + + return (buttonsText == "") ? null : buttonsText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + } + + private void ParseErrorCommonArg() + { + ErrorCommonArg errorCommonArg = IApplet.ReadStruct<ErrorCommonArg>(_errorStorage); + + uint module = errorCommonArg.Module; + uint description = errorCommonArg.Description; + + if (_errorCommonHeader.MessageFlag == 0) + { + (module, description) = HexToResultCode(errorCommonArg.ResultCode); + } + + string message = GetMessageText(module, description, "DlgMsg"); + + if (message == "") + { + message = "An error has occured.\n\n" + + "Please try again later.\n\n" + + "If the problem persists, please refer to the Ryujinx website.\n" + + "www.ryujinx.org"; + } + + string[] buttons = GetButtonsText(module, description, "DlgBtn"); + + bool showDetails = _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Error Code: {module}-{description:0000}", "\n" + message, buttons); + if (showDetails) + { + message = GetMessageText(module, description, "FlvMsg"); + buttons = GetButtonsText(module, description, "FlvBtn"); + + _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Details: {module}-{description:0000}", "\n" + message, buttons); + } + } + + private void ParseApplicationErrorArg() + { + ApplicationErrorArg applicationErrorArg = IApplet.ReadStruct<ApplicationErrorArg>(_errorStorage); + + byte[] messageTextBuffer = new byte[0x800]; + byte[] detailsTextBuffer = new byte[0x800]; + + applicationErrorArg.MessageText.AsSpan().CopyTo(messageTextBuffer); + applicationErrorArg.DetailsText.AsSpan().CopyTo(detailsTextBuffer); + + string messageText = Encoding.ASCII.GetString(messageTextBuffer.TakeWhile(b => !b.Equals(0)).ToArray()); + string detailsText = Encoding.ASCII.GetString(detailsTextBuffer.TakeWhile(b => !b.Equals(0)).ToArray()); + + List<string> buttons = new List<string>(); + + // TODO: Handle the LanguageCode to return the translated "OK" and "Details". + + if (detailsText.Trim() != "") + { + buttons.Add("Details"); + } + + buttons.Add("OK"); + + bool showDetails = _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Error Number: {applicationErrorArg.ErrorNumber}", "\n" + messageText, buttons.ToArray()); + if (showDetails) + { + buttons.RemoveAt(0); + + _horizon.Device.UiHandler.DisplayErrorAppletDialog($"Error Number: {applicationErrorArg.ErrorNumber} (Details)", "\n" + detailsText, buttons.ToArray()); + } + } + + public ResultCode GetResult() + { + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonArg.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonArg.cs new file mode 100644 index 00000000..530a2ad8 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonArg.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.Error +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ErrorCommonArg + { + public uint Module; + public uint Description; + public uint ResultCode; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonHeader.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonHeader.cs new file mode 100644 index 00000000..b93cdd4f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorCommonHeader.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.Error +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ErrorCommonHeader + { + public ErrorType Type; + public byte JumpFlag; + public byte ReservedFlag1; + public byte ReservedFlag2; + public byte ReservedFlag3; + public byte ContextFlag; + public byte MessageFlag; + public byte ContextFlag2; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorType.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorType.cs new file mode 100644 index 00000000..f06af1d3 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorType.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.HLE.HOS.Applets.Error +{ + enum ErrorType : byte + { + ErrorCommonArg, + SystemErrorArg, + ApplicationErrorArg, + ErrorEulaArg, + ErrorPctlArg, + ErrorRecordArg, + SystemUpdateEulaArg = 8 + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/IApplet.cs b/src/Ryujinx.HLE/HOS/Applets/IApplet.cs new file mode 100644 index 00000000..224d6787 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/IApplet.cs @@ -0,0 +1,28 @@ +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using Ryujinx.HLE.Ui; +using Ryujinx.Memory; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets +{ + interface IApplet + { + event EventHandler AppletStateChanged; + + ResultCode Start(AppletSession normalSession, + AppletSession interactiveSession); + + ResultCode GetResult(); + + bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position) + { + return false; + } + + static T ReadStruct<T>(ReadOnlySpan<byte> data) where T : unmanaged + { + return MemoryMarshal.Cast<byte, T>(data)[0]; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs new file mode 100644 index 00000000..a8119a47 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs @@ -0,0 +1,58 @@ +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using System; +using System.IO; + +namespace Ryujinx.HLE.HOS.Applets +{ + internal class PlayerSelectApplet : IApplet + { + private Horizon _system; + + private AppletSession _normalSession; + private AppletSession _interactiveSession; + + public event EventHandler AppletStateChanged; + + public PlayerSelectApplet(Horizon system) + { + _system = system; + } + + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) + { + _normalSession = normalSession; + _interactiveSession = interactiveSession; + + // TODO(jduncanator): Parse PlayerSelectConfig from input data + _normalSession.Push(BuildResponse()); + + AppletStateChanged?.Invoke(this, null); + + _system.ReturnFocus(); + + return ResultCode.Success; + } + + public ResultCode GetResult() + { + return ResultCode.Success; + } + + private byte[] BuildResponse() + { + UserProfile currentUser = _system.AccountManager.LastOpenedUser; + + using (MemoryStream stream = MemoryStreamManager.Shared.GetStream()) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.Write((ulong)PlayerSelectResult.Success); + + currentUser.UserId.Write(writer); + + return stream.ToArray(); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectResult.cs b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectResult.cs new file mode 100644 index 00000000..682e094e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectResult.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Applets +{ + enum PlayerSelectResult : ulong + { + Success = 0, + Failure = 2 + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InitialCursorPosition.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InitialCursorPosition.cs new file mode 100644 index 00000000..727b6d27 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InitialCursorPosition.cs @@ -0,0 +1,18 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Identifies the initial position of the cursor displayed in the area. + /// </summary> + enum InitialCursorPosition : uint + { + /// <summary> + /// Position the cursor at the beginning of the text + /// </summary> + Start, + + /// <summary> + /// Position the cursor at the end of the text + /// </summary> + End + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs new file mode 100644 index 00000000..b17debfc --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs @@ -0,0 +1,48 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Possible requests to the software keyboard when running in inline mode. + /// </summary> + enum InlineKeyboardRequest : uint + { + /// <summary> + /// Finalize the keyboard applet. + /// </summary> + Finalize = 0x4, + + /// <summary> + /// Set user words for text prediction. + /// </summary> + SetUserWordInfo = 0x6, + + /// <summary> + /// Sets the CustomizeDic data. Can't be used if CustomizedDictionaries is already set. + /// </summary> + SetCustomizeDic = 0x7, + + /// <summary> + /// Configure the keyboard applet and put it in a state where it is processing input. + /// </summary> + Calc = 0xA, + + /// <summary> + /// Set custom dictionaries for text prediction. Can't be used if SetCustomizeDic is already set. + /// </summary> + SetCustomizedDictionaries = 0xB, + + /// <summary> + /// Release custom dictionaries data. + /// </summary> + UnsetCustomizedDictionaries = 0xC, + + /// <summary> + /// [8.0.0+] Request the keyboard applet to use the ChangedStringV2 response when notifying changes in text data. + /// </summary> + UseChangedStringV2 = 0xD, + + /// <summary> + /// [8.0.0+] Request the keyboard applet to use the MovedCursorV2 response when notifying changes in cursor position. + /// </summary> + UseMovedCursorV2 = 0xE + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs new file mode 100644 index 00000000..b21db507 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs @@ -0,0 +1,93 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Possible responses from the software keyboard when running in inline mode. + /// </summary> + enum InlineKeyboardResponse : uint + { + /// <summary> + /// The software keyboard received a Calc and it is fully initialized. Reply data is ignored by the user-process. + /// </summary> + FinishedInitialize = 0x0, + + /// <summary> + /// Default response. Official sw has no handling for this besides just closing the storage. + /// </summary> + Default = 0x1, + + /// <summary> + /// The text data in the software keyboard changed (UTF-16 encoding). + /// </summary> + ChangedString = 0x2, + + /// <summary> + /// The cursor position in the software keyboard changed (UTF-16 encoding). + /// </summary> + MovedCursor = 0x3, + + /// <summary> + /// A tab in the software keyboard changed. + /// </summary> + MovedTab = 0x4, + + /// <summary> + /// The OK key was pressed in the software keyboard, confirming the input text (UTF-16 encoding). + /// </summary> + DecidedEnter = 0x5, + + /// <summary> + /// The Cancel key was pressed in the software keyboard, cancelling the input. + /// </summary> + DecidedCancel = 0x6, + + /// <summary> + /// Same as ChangedString, but with UTF-8 encoding. + /// </summary> + ChangedStringUtf8 = 0x7, + + /// <summary> + /// Same as MovedCursor, but with UTF-8 encoding. + /// </summary> + MovedCursorUtf8 = 0x8, + + /// <summary> + /// Same as DecidedEnter, but with UTF-8 encoding. + /// </summary> + DecidedEnterUtf8 = 0x9, + + /// <summary> + /// They software keyboard is releasing the data previously set by a SetCustomizeDic request. + /// </summary> + UnsetCustomizeDic = 0xA, + + /// <summary> + /// They software keyboard is releasing the data previously set by a SetUserWordInfo request. + /// </summary> + ReleasedUserWordInfo = 0xB, + + /// <summary> + /// They software keyboard is releasing the data previously set by a SetCustomizedDictionaries request. + /// </summary> + UnsetCustomizedDictionaries = 0xC, + + /// <summary> + /// Same as ChangedString, but with additional fields. + /// </summary> + ChangedStringV2 = 0xD, + + /// <summary> + /// Same as MovedCursor, but with additional fields. + /// </summary> + MovedCursorV2 = 0xE, + + /// <summary> + /// Same as ChangedStringUtf8, but with additional fields. + /// </summary> + ChangedStringUtf8V2 = 0xF, + + /// <summary> + /// Same as MovedCursorUtf8, but with additional fields. + /// </summary> + MovedCursorUtf8V2 = 0x10 + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs new file mode 100644 index 00000000..47e1a774 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs @@ -0,0 +1,33 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Possible states for the software keyboard when running in inline mode. + /// </summary> + enum InlineKeyboardState : uint + { + /// <summary> + /// The software keyboard has just been created or finalized and is uninitialized. + /// </summary> + Uninitialized = 0x0, + + /// <summary> + /// The software keyboard is initialized, but it is not visible and not processing input. + /// </summary> + Initialized = 0x1, + + /// <summary> + /// The software keyboard is transitioning to a visible state. + /// </summary> + Appearing = 0x2, + + /// <summary> + /// The software keyboard is visible and receiving processing input. + /// </summary> + Shown = 0x3, + + /// <summary> + /// software keyboard is transitioning to a hidden state because the user pressed either OK or Cancel. + /// </summary> + Disappearing = 0x4 + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs new file mode 100644 index 00000000..c3e45d46 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs @@ -0,0 +1,298 @@ +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, 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, uint cursor, BinaryWriter writer, uint maxSize, Encoding encoding, bool padMiddle) + { + uint length = WriteString(text, writer, maxSize, encoding); + + if (cursor > length) + { + cursor = length; + } + + if (padMiddle) + { + writer.Write((int)-1); // ? + writer.Write((int)-1); // ? + } + + writer.Write(cursor); // Cursor position + } + + public static byte[] FinishedInitialize(InlineKeyboardState state) + { + uint resSize = 2 * sizeof(uint) + 0x1; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.FinishedInitialize, writer); + writer.Write((byte)1); // Data (ignored by the program) + + return stream.ToArray(); + } + } + + public static byte[] Default(InlineKeyboardState state) + { + uint resSize = 2 * sizeof(uint); + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.Default, writer); + + return stream.ToArray(); + } + } + + public static byte[] ChangedString(string text, uint cursor, InlineKeyboardState state) + { + uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.ChangedString, writer); + WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode, true); + + return stream.ToArray(); + } + } + + public static byte[] MovedCursor(string text, uint cursor, InlineKeyboardState state) + { + uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.MovedCursor, writer); + WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode, false); + + return stream.ToArray(); + } + } + + public static byte[] MovedTab(string text, uint cursor, InlineKeyboardState state) + { + // 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(state, InlineKeyboardResponse.MovedTab, writer); + WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode, false); + + return stream.ToArray(); + } + } + + public static byte[] DecidedEnter(string text, InlineKeyboardState state) + { + uint resSize = 3 * sizeof(uint) + MaxStrLenUTF16; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.DecidedEnter, writer); + WriteString(text, writer, MaxStrLenUTF16, Encoding.Unicode); + + return stream.ToArray(); + } + } + + public static byte[] DecidedCancel(InlineKeyboardState state) + { + uint resSize = 2 * sizeof(uint); + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.DecidedCancel, writer); + + return stream.ToArray(); + } + } + + public static byte[] ChangedStringUtf8(string text, uint cursor, InlineKeyboardState state) + { + uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8, writer); + WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF8, Encoding.UTF8, true); + + return stream.ToArray(); + } + } + + public static byte[] MovedCursorUtf8(string text, uint cursor, InlineKeyboardState state) + { + uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8, writer); + WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF8, Encoding.UTF8, false); + + return stream.ToArray(); + } + } + + public static byte[] DecidedEnterUtf8(string text, InlineKeyboardState state) + { + uint resSize = 3 * sizeof(uint) + MaxStrLenUTF8; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.DecidedEnterUtf8, writer); + WriteString(text, writer, MaxStrLenUTF8, Encoding.UTF8); + + return stream.ToArray(); + } + } + + public static byte[] UnsetCustomizeDic(InlineKeyboardState state) + { + uint resSize = 2 * sizeof(uint); + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.UnsetCustomizeDic, writer); + + return stream.ToArray(); + } + } + + public static byte[] ReleasedUserWordInfo(InlineKeyboardState state) + { + uint resSize = 2 * sizeof(uint); + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.ReleasedUserWordInfo, writer); + + return stream.ToArray(); + } + } + + public static byte[] UnsetCustomizedDictionaries(InlineKeyboardState state) + { + uint resSize = 2 * sizeof(uint); + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.UnsetCustomizedDictionaries, writer); + + return stream.ToArray(); + } + } + + public static byte[] ChangedStringV2(string text, uint cursor, InlineKeyboardState state) + { + uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16 + 0x1; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.ChangedStringV2, writer); + WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode, true); + writer.Write((byte)0); // Flag == 0 + + return stream.ToArray(); + } + } + + public static byte[] MovedCursorV2(string text, uint cursor, InlineKeyboardState state) + { + uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16 + 0x1; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.MovedCursorV2, writer); + WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF16, Encoding.Unicode, false); + writer.Write((byte)0); // Flag == 0 + + return stream.ToArray(); + } + } + + public static byte[] ChangedStringUtf8V2(string text, uint cursor, InlineKeyboardState state) + { + uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8 + 0x1; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8V2, writer); + WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF8, Encoding.UTF8, true); + writer.Write((byte)0); // Flag == 0 + + return stream.ToArray(); + } + } + + public static byte[] MovedCursorUtf8V2(string text, uint cursor, InlineKeyboardState state) + { + uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8 + 0x1; + + using (MemoryStream stream = new MemoryStream(new byte[resSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8V2, writer); + WriteStringWithCursor(text, cursor, writer, MaxStrLenUTF8, Encoding.UTF8, false); + writer.Write((byte)0); // Flag == 0 + + return stream.ToArray(); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InputFormMode.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InputFormMode.cs new file mode 100644 index 00000000..c3ce2c12 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InputFormMode.cs @@ -0,0 +1,18 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Identifies the text entry mode. + /// </summary> + enum InputFormMode : uint + { + /// <summary> + /// Displays the text entry area as a single-line field. + /// </summary> + SingleLine, + + /// <summary> + /// Displays the text entry area as a multi-line field. + /// </summary> + MultiLine + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InvalidButtonFlags.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InvalidButtonFlags.cs new file mode 100644 index 00000000..1166e81d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InvalidButtonFlags.cs @@ -0,0 +1,17 @@ +using System; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Identifies prohibited buttons. + /// </summary> + [Flags] + enum InvalidButtonFlags : uint + { + None = 0, + AnalogStickL = 1 << 1, + AnalogStickR = 1 << 2, + ZL = 1 << 3, + ZR = 1 << 4, + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InvalidCharFlags.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InvalidCharFlags.cs new file mode 100644 index 00000000..f3fd8ac8 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InvalidCharFlags.cs @@ -0,0 +1,56 @@ +using System; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Identifies prohibited character sets. + /// </summary> + [Flags] + enum InvalidCharFlags : uint + { + /// <summary> + /// No characters are prohibited. + /// </summary> + None = 0 << 1, + + /// <summary> + /// Prohibits spaces. + /// </summary> + Space = 1 << 1, + + /// <summary> + /// Prohibits the at (@) symbol. + /// </summary> + AtSymbol = 1 << 2, + + /// <summary> + /// Prohibits the percent (%) symbol. + /// </summary> + Percent = 1 << 3, + + /// <summary> + /// Prohibits the forward slash (/) symbol. + /// </summary> + ForwardSlash = 1 << 4, + + /// <summary> + /// Prohibits the backward slash (\) symbol. + /// </summary> + BackSlash = 1 << 5, + + /// <summary> + /// Prohibits numbers. + /// </summary> + Numbers = 1 << 6, + + /// <summary> + /// Prohibits characters outside of those allowed in download codes. + /// </summary> + DownloadCode = 1 << 7, + + /// <summary> + /// Prohibits characters outside of those allowed in Mii Nicknames. + /// </summary> + Username = 1 << 8 + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardCalcFlags.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardCalcFlags.cs new file mode 100644 index 00000000..0b0f138b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardCalcFlags.cs @@ -0,0 +1,26 @@ +using System; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Bitmask of commands encoded in the Flags field of the Calc structs. + /// </summary> + [Flags] + enum KeyboardCalcFlags : ulong + { + Initialize = 0x1, + SetVolume = 0x2, + Appear = 0x4, + SetInputText = 0x8, + SetCursorPos = 0x10, + SetUtf8Mode = 0x20, + SetKeyboardBackground = 0x100, + SetKeyboardOptions1 = 0x200, + SetKeyboardOptions2 = 0x800, + EnableSeGroup = 0x2000, + DisableSeGroup = 0x4000, + SetBackspaceEnabled = 0x8000, + AppearTrigger = 0x10000, + MustShow = Appear | SetInputText | AppearTrigger + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardInputMode.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardInputMode.cs new file mode 100644 index 00000000..925d52f6 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardInputMode.cs @@ -0,0 +1,14 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Active input options set by the keyboard applet. These options allow keyboard + /// players to input text without conflicting with the controller mappings. + /// </summary> + enum KeyboardInputMode : uint + { + ControllerAndKeyboard, + KeyboardOnly, + ControllerOnly, + Count, + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardMiniaturizationMode.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardMiniaturizationMode.cs new file mode 100644 index 00000000..5184118c --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardMiniaturizationMode.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// The miniaturization mode used by the keyboard in inline mode. + /// </summary> + enum KeyboardMiniaturizationMode : byte + { + None = 0, + Auto = 1, + Forced = 2 + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardMode.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardMode.cs new file mode 100644 index 00000000..f512050e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardMode.cs @@ -0,0 +1,31 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Identifies the variant of keyboard displayed on screen. + /// </summary> + enum KeyboardMode : uint + { + /// <summary> + /// A full alpha-numeric keyboard. + /// </summary> + Default = 0, + + /// <summary> + /// Number pad. + /// </summary> + NumbersOnly = 1, + + /// <summary> + /// ASCII characters keyboard. + /// </summary> + ASCII = 2, + + FullLatin = 3, + Alphabet = 4, + SimplifiedChinese = 5, + TraditionalChinese = 6, + Korean = 7, + LanguageSet2 = 8, + LanguageSet2Latin = 9, + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardResult.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardResult.cs new file mode 100644 index 00000000..4f570d3f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/KeyboardResult.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// The intention of the user when they finish the interaction with the keyboard. + /// </summary> + enum KeyboardResult + { + NotSet = 0, + Accept = 1, + Cancel = 2, + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/PasswordMode.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/PasswordMode.cs new file mode 100644 index 00000000..fc9e1ff8 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/PasswordMode.cs @@ -0,0 +1,18 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Identifies the display mode of text in a password field. + /// </summary> + enum PasswordMode : uint + { + /// <summary> + /// Display input characters. + /// </summary> + Disabled, + + /// <summary> + /// Hide input characters. + /// </summary> + Enabled + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnA.png b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnA.png Binary files differnew file mode 100644 index 00000000..a8ee784d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnA.png diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnA.svg b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnA.svg new file mode 100644 index 00000000..6257fd12 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnA.svg @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="32" + height="32" + viewBox="0 0 8.4666665 8.4666669" + version="1.1" + id="svg8" + inkscape:export-filename="C:\Users\caian\source\repos\Ryujinx\Ryujinx.HLE\HOS\Applets\SoftwareKeyboard\Resources\Icon_Accept.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" + sodipodi:docname="buttons_ab.svg" + inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="15.839192" + inkscape:cx="16.591066" + inkscape:cy="14.090021" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + inkscape:document-rotation="0" + showgrid="false" + units="px" + showguides="false" + inkscape:window-width="1267" + inkscape:window-height="976" + inkscape:window-x="242" + inkscape:window-y="34" + inkscape:window-maximized="0" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <circle + style="fill:#ffffff;stroke-width:1.57002;fill-opacity:1" + id="path839" + cx="4.2333331" + cy="4.2333331" + r="4.2333331" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;font-size:7.02011px;line-height:1.25;font-family:sans-serif;fill:#4b4b4b;fill-opacity:1;stroke:none;stroke-width:0.376071" + x="1.9222834" + y="6.5921373" + id="text835-2" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" + inkscape:export-filename="C:\Users\caian\source\repos\Ryujinx\Ryujinx.HLE\HOS\Applets\SoftwareKeyboard\Resources\text835-2.png"><tspan + sodipodi:role="line" + id="tspan833-9" + x="1.9222834" + y="6.5921373" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.02011px;font-family:Arial;-inkscape-font-specification:Arial;fill:#4b4b4b;fill-opacity:1;stroke-width:0.376071">A</tspan></text> + </g> +</svg> diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnB.png b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnB.png Binary files differnew file mode 100644 index 00000000..e1fa3454 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnB.png diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnB.svg b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnB.svg new file mode 100644 index 00000000..ea6bb9bd --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_BtnB.svg @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="32" + height="32" + viewBox="0 0 8.4666665 8.4666669" + version="1.1" + id="svg8" + inkscape:export-filename="C:\Users\caian\source\repos\Ryujinx\Ryujinx.HLE\HOS\Applets\SoftwareKeyboard\Resources\Icon_Accept.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" + sodipodi:docname="buttons_ab.svg" + inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="15.839192" + inkscape:cx="16.591066" + inkscape:cy="14.090021" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + inkscape:document-rotation="0" + showgrid="false" + units="px" + showguides="false" + inkscape:window-width="1267" + inkscape:window-height="976" + inkscape:window-x="242" + inkscape:window-y="34" + inkscape:window-maximized="0" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <circle + style="fill:#ffffff;stroke-width:1.57002;fill-opacity:1" + id="path839" + cx="4.2333331" + cy="4.2333331" + r="4.2333331" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;font-size:7.02012px;line-height:1.25;font-family:sans-serif;fill:#4b4b4b;fill-opacity:1;stroke:none;stroke-width:0.37607" + x="2.0223334" + y="6.6920195" + id="text835" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96"><tspan + sodipodi:role="line" + id="tspan833" + x="2.0223334" + y="6.6920195" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.02012px;font-family:Arial;-inkscape-font-specification:Arial;fill:#4b4b4b;fill-opacity:1;stroke-width:0.37607">B</tspan></text> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;font-size:7.02011px;line-height:1.25;font-family:sans-serif;fill:#4b4b4b;fill-opacity:1;stroke:none;stroke-width:0.376071" + x="2.0223367" + y="6.6920156" + id="text835-2" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" + inkscape:export-filename="C:\Users\caian\source\repos\Ryujinx\Ryujinx.HLE\HOS\Applets\SoftwareKeyboard\Resources\text835-2.png"><tspan + sodipodi:role="line" + id="tspan833-9" + x="2.0223367" + y="6.6920156" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.02011px;font-family:Arial;-inkscape-font-specification:Arial;fill:#4b4b4b;fill-opacity:1;stroke-width:0.376071">B</tspan></text> + </g> +</svg> diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_KeyF6.png b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_KeyF6.png Binary files differnew file mode 100644 index 00000000..d6dbdc1a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_KeyF6.png diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_KeyF6.svg b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_KeyF6.svg new file mode 100644 index 00000000..2256ebeb --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Icon_KeyF6.svg @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="32" + height="32" + viewBox="0 0 8.4666665 8.4666669" + version="1.1" + id="svg8" + inkscape:export-filename="C:\Users\caian\source\repos\Ryujinx\Ryujinx.HLE\HOS\Applets\SoftwareKeyboard\Resources\Icon_KeyF5.png" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" + sodipodi:docname="Icon_KeyF5.svg" + inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"> + <defs + id="defs2" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="15.839192" + inkscape:cx="16.591066" + inkscape:cy="14.090021" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + inkscape:document-rotation="0" + showgrid="false" + units="px" + showguides="false" + inkscape:window-width="1267" + inkscape:window-height="976" + inkscape:window-x="242" + inkscape:window-y="25" + inkscape:window-maximized="0" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1"> + <rect + style="fill:#ffffff;stroke-width:2.21199" + id="rect837" + width="8.4666662" + height="8.4666662" + x="1.3877788e-17" + y="0" /> + <text + xml:space="preserve" + style="font-style:normal;font-weight:normal;font-size:4.23333px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264578" + x="1.0762799" + y="4.2016153" + id="text835" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" + transform="scale(0.9999825,1.0000175)"><tspan + sodipodi:role="line" + id="tspan833" + x="1.0762799" + y="4.2016153" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:4.23333px;font-family:Consolas;-inkscape-font-specification:Consolas;stroke-width:0.264578">F6</tspan></text> + <rect + style="fill:none;fill-opacity:1;stroke:#757575;stroke-width:0.26458333;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" + id="rect891" + width="6.9844265" + height="6.984426" + x="0.74112016" + y="0.47653681" /> + <path + style="fill:none;stroke:#757575;stroke-width:0.264583px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="M 0,0 0.74112016,0.47653681" + id="path895" + sodipodi:nodetypes="cc" /> + <path + style="fill:none;stroke:#757575;stroke-width:0.264583px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="M 8.4666662,0 7.7255461,0.47653681" + id="path897" + sodipodi:nodetypes="cc" /> + <path + style="fill:none;stroke:#757575;stroke-width:0.264583px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="M 7.3685303e-7,8.4666667 0.7411209,7.4609628" + id="path901" + sodipodi:nodetypes="cc" /> + <path + style="fill:none;stroke:#757575;stroke-width:0.264583px;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1" + d="M 8.4666669,8.4666667 7.7255468,7.4609628" + id="path903" + sodipodi:nodetypes="cc" /> + </g> +</svg> diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Logo_Ryujinx.png b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Logo_Ryujinx.png Binary files differnew file mode 100644 index 00000000..0e8da15e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/Resources/Logo_Ryujinx.png diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs new file mode 100644 index 00000000..e1ee0507 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs @@ -0,0 +1,119 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// A structure with appearance configurations for the software keyboard when running in inline mode. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)] + struct SoftwareKeyboardAppear + { + public const int OkTextLength = SoftwareKeyboardAppearEx.OkTextLength; + + public KeyboardMode KeyboardMode; + + /// <summary> + /// The string displayed in the Submit button. + /// </summary> + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = OkTextLength + 1)] + public string OkText; + + /// <summary> + /// The character displayed in the left button of the numeric keyboard. + /// </summary> + public char LeftOptionalSymbolKey; + + /// <summary> + /// The character displayed in the right button of the numeric keyboard. + /// </summary> + public char RightOptionalSymbolKey; + + /// <summary> + /// When set, predictive typing is enabled making use of the system dictionary, and any custom user dictionary. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool PredictionEnabled; + + /// <summary> + /// When set, there is only the option to accept the input. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool CancelButtonDisabled; + + /// <summary> + /// Specifies prohibited characters that cannot be input into the text entry area. + /// </summary> + public InvalidCharFlags InvalidChars; + + /// <summary> + /// Maximum text length allowed. + /// </summary> + public int TextMaxLength; + + /// <summary> + /// Minimum text length allowed. + /// </summary> + public int TextMinLength; + + /// <summary> + /// Indicates the return button is enabled in the keyboard. This allows for input with multiple lines. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool UseNewLine; + + /// <summary> + /// [10.0.0+] If value is 1 or 2, then keytopAsFloating=0 and footerScalable=1 in Calc. + /// </summary> + public KeyboardMiniaturizationMode MiniaturizationMode; + + public byte Reserved1; + public byte Reserved2; + + /// <summary> + /// Bit field with invalid buttons for the keyboard. + /// </summary> + public InvalidButtonFlags InvalidButtons; + + [MarshalAs(UnmanagedType.I1)] + public bool UseSaveData; + + public uint Reserved3; + public ushort Reserved4; + public byte Reserved5; + public ulong Reserved6; + public ulong Reserved7; + + public SoftwareKeyboardAppearEx ToExtended() + { + SoftwareKeyboardAppearEx appear = new SoftwareKeyboardAppearEx(); + + appear.KeyboardMode = KeyboardMode; + appear.OkText = OkText; + appear.LeftOptionalSymbolKey = LeftOptionalSymbolKey; + appear.RightOptionalSymbolKey = RightOptionalSymbolKey; + appear.PredictionEnabled = PredictionEnabled; + appear.CancelButtonDisabled = CancelButtonDisabled; + appear.InvalidChars = InvalidChars; + appear.TextMaxLength = TextMaxLength; + appear.TextMinLength = TextMinLength; + appear.UseNewLine = UseNewLine; + appear.MiniaturizationMode = MiniaturizationMode; + appear.Reserved1 = Reserved1; + appear.Reserved2 = Reserved2; + appear.InvalidButtons = InvalidButtons; + appear.UseSaveData = UseSaveData; + appear.Reserved3 = Reserved3; + appear.Reserved4 = Reserved4; + appear.Reserved5 = Reserved5; + appear.Uid0 = Reserved6; + appear.Uid1 = Reserved7; + appear.SamplingNumber = 0; + appear.Reserved6 = 0; + appear.Reserved7 = 0; + appear.Reserved8 = 0; + appear.Reserved9 = 0; + + return appear; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppearEx.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppearEx.cs new file mode 100644 index 00000000..d1756b07 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppearEx.cs @@ -0,0 +1,100 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// A structure with appearance configurations for the software keyboard when running in inline mode. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)] + struct SoftwareKeyboardAppearEx + { + public const int OkTextLength = 8; + + public KeyboardMode KeyboardMode; + + /// <summary> + /// The string displayed in the Submit button. + /// </summary> + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = OkTextLength + 1)] + public string OkText; + + /// <summary> + /// The character displayed in the left button of the numeric keyboard. + /// </summary> + public char LeftOptionalSymbolKey; + + /// <summary> + /// The character displayed in the right button of the numeric keyboard. + /// </summary> + public char RightOptionalSymbolKey; + + /// <summary> + /// When set, predictive typing is enabled making use of the system dictionary, and any custom user dictionary. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool PredictionEnabled; + + /// <summary> + /// When set, there is only the option to accept the input. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool CancelButtonDisabled; + + /// <summary> + /// Specifies prohibited characters that cannot be input into the text entry area. + /// </summary> + public InvalidCharFlags InvalidChars; + + /// <summary> + /// Maximum text length allowed. + /// </summary> + public int TextMaxLength; + + /// <summary> + /// Minimum text length allowed. + /// </summary> + public int TextMinLength; + + /// <summary> + /// Indicates the return button is enabled in the keyboard. This allows for input with multiple lines. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool UseNewLine; + + /// <summary> + /// [10.0.0+] If value is 1 or 2, then keytopAsFloating=0 and footerScalable=1 in Calc. + /// </summary> + public KeyboardMiniaturizationMode MiniaturizationMode; + + public byte Reserved1; + public byte Reserved2; + + /// <summary> + /// Bit field with invalid buttons for the keyboard. + /// </summary> + public InvalidButtonFlags InvalidButtons; + + [MarshalAs(UnmanagedType.I1)] + public bool UseSaveData; + + public uint Reserved3; + public ushort Reserved4; + public byte Reserved5; + + /// <summary> + /// The id of the user associated with the appear request. + /// </summary> + public ulong Uid0; + public ulong Uid1; + + /// <summary> + /// The sampling number for the keyboard appearance. + /// </summary> + public ulong SamplingNumber; + + public ulong Reserved6; + public ulong Reserved7; + public ulong Reserved8; + public ulong Reserved9; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs new file mode 100644 index 00000000..278ea56c --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs @@ -0,0 +1,816 @@ +using Ryujinx.Common; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad; +using Ryujinx.HLE.Ui; +using Ryujinx.HLE.Ui.Input; +using Ryujinx.Memory; +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.HLE.HOS.Applets +{ + internal class SoftwareKeyboardApplet : IApplet + { + private const string DefaultInputText = "Ryujinx"; + + private const int StandardBufferSize = 0x7D8; + private const int InteractiveBufferSize = 0x7D4; + private const int MaxUserWords = 0x1388; + private const int MaxUiTextSize = 100; + + private const Key CycleInputModesKey = Key.F6; + + private readonly Switch _device; + + private SoftwareKeyboardState _foregroundState = SoftwareKeyboardState.Uninitialized; + private volatile InlineKeyboardState _backgroundState = InlineKeyboardState.Uninitialized; + + private bool _isBackground = false; + + private AppletSession _normalSession; + private AppletSession _interactiveSession; + + // Configuration for foreground mode. + private SoftwareKeyboardConfig _keyboardForegroundConfig; + + // Configuration for background (inline) mode. + private SoftwareKeyboardInitialize _keyboardBackgroundInitialize; + private SoftwareKeyboardCustomizeDic _keyboardBackgroundDic; + private SoftwareKeyboardDictSet _keyboardBackgroundDictSet; + private SoftwareKeyboardUserWord[] _keyboardBackgroundUserWords; + + private byte[] _transferMemory; + + private string _textValue = ""; + private int _cursorBegin = 0; + private Encoding _encoding = Encoding.Unicode; + private KeyboardResult _lastResult = KeyboardResult.NotSet; + + private IDynamicTextInputHandler _dynamicTextInputHandler = null; + private SoftwareKeyboardRenderer _keyboardRenderer = null; + private NpadReader _npads = null; + private bool _canAcceptController = false; + private KeyboardInputMode _inputMode = KeyboardInputMode.ControllerAndKeyboard; + + private object _lock = new object(); + + public event EventHandler AppletStateChanged; + + public SoftwareKeyboardApplet(Horizon system) + { + _device = system.Device; + } + + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) + { + lock (_lock) + { + _normalSession = normalSession; + _interactiveSession = interactiveSession; + + _interactiveSession.DataAvailable += OnInteractiveData; + + var launchParams = _normalSession.Pop(); + var keyboardConfig = _normalSession.Pop(); + + _isBackground = keyboardConfig.Length == Unsafe.SizeOf<SoftwareKeyboardInitialize>(); + + if (_isBackground) + { + // Initialize the keyboard applet in background mode. + + _keyboardBackgroundInitialize = MemoryMarshal.Read<SoftwareKeyboardInitialize>(keyboardConfig); + _backgroundState = InlineKeyboardState.Uninitialized; + + if (_device.UiHandler == null) + { + Logger.Error?.Print(LogClass.ServiceAm, "GUI Handler is not set, software keyboard applet will not work properly"); + } + else + { + // Create a text handler that converts keyboard strokes to strings. + _dynamicTextInputHandler = _device.UiHandler.CreateDynamicTextInputHandler(); + _dynamicTextInputHandler.TextChangedEvent += HandleTextChangedEvent; + _dynamicTextInputHandler.KeyPressedEvent += HandleKeyPressedEvent; + + _npads = new NpadReader(_device); + _npads.NpadButtonDownEvent += HandleNpadButtonDownEvent; + _npads.NpadButtonUpEvent += HandleNpadButtonUpEvent; + + _keyboardRenderer = new SoftwareKeyboardRenderer(_device.UiHandler.HostUiTheme); + } + + return ResultCode.Success; + } + else + { + // Initialize the keyboard applet in foreground mode. + + if (keyboardConfig.Length < Marshal.SizeOf<SoftwareKeyboardConfig>()) + { + Logger.Error?.Print(LogClass.ServiceAm, $"SoftwareKeyboardConfig size mismatch. Expected {Marshal.SizeOf<SoftwareKeyboardConfig>():x}. Got {keyboardConfig.Length:x}"); + } + else + { + _keyboardForegroundConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig); + } + + if (!_normalSession.TryPop(out _transferMemory)) + { + Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null"); + } + + if (_keyboardForegroundConfig.UseUtf8) + { + _encoding = Encoding.UTF8; + } + + _foregroundState = SoftwareKeyboardState.Ready; + + ExecuteForegroundKeyboard(); + + return ResultCode.Success; + } + } + } + + public ResultCode GetResult() + { + return ResultCode.Success; + } + + private bool IsKeyboardActive() + { + return _backgroundState >= InlineKeyboardState.Appearing && _backgroundState < InlineKeyboardState.Disappearing; + } + + private bool InputModeControllerEnabled() + { + return _inputMode == KeyboardInputMode.ControllerAndKeyboard || + _inputMode == KeyboardInputMode.ControllerOnly; + } + + private bool InputModeTypingEnabled() + { + return _inputMode == KeyboardInputMode.ControllerAndKeyboard || + _inputMode == KeyboardInputMode.KeyboardOnly; + } + + private void AdvanceInputMode() + { + _inputMode = (KeyboardInputMode)((int)(_inputMode + 1) % (int)KeyboardInputMode.Count); + } + + public bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position) + { + _npads?.Update(); + + _keyboardRenderer?.SetSurfaceInfo(surfaceInfo); + + return _keyboardRenderer?.DrawTo(destination, position) ?? false; + } + + 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 && _keyboardForegroundConfig.InitialStringLength > 0) + { + initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardForegroundConfig.InitialStringOffset, + 2 * _keyboardForegroundConfig.InitialStringLength); + } + + // If the max string length is 0, we set it to a large default + // length. + if (_keyboardForegroundConfig.StringLengthMax == 0) + { + _keyboardForegroundConfig.StringLengthMax = 100; + } + + if (_device.UiHandler == null) + { + Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default"); + + _textValue = DefaultInputText; + _lastResult = KeyboardResult.Accept; + } + else + { + // Call the configured GUI handler to get user's input. + var args = new SoftwareKeyboardUiArgs + { + HeaderText = StripUnicodeControlCodes(_keyboardForegroundConfig.HeaderText), + SubtitleText = StripUnicodeControlCodes(_keyboardForegroundConfig.SubtitleText), + GuideText = StripUnicodeControlCodes(_keyboardForegroundConfig.GuideText), + SubmitText = (!string.IsNullOrWhiteSpace(_keyboardForegroundConfig.SubmitText) ? + _keyboardForegroundConfig.SubmitText : "OK"), + StringLengthMin = _keyboardForegroundConfig.StringLengthMin, + StringLengthMax = _keyboardForegroundConfig.StringLengthMax, + InitialText = initialText + }; + + _lastResult = _device.UiHandler.DisplayInputDialog(args, out _textValue) ? KeyboardResult.Accept : KeyboardResult.Cancel; + _textValue ??= initialText ?? DefaultInputText; + } + + // 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. + // This should always be done before the text truncation step. + while (_textValue.Length < _keyboardForegroundConfig.StringLengthMin) + { + _textValue = String.Join(" ", _textValue, _textValue); + } + + // If our default text is longer than the allowed length, + // we truncate it. + if (_textValue.Length > _keyboardForegroundConfig.StringLengthMax) + { + _textValue = _textValue.Substring(0, _keyboardForegroundConfig.StringLengthMax); + } + + // Does the application want to validate the text itself? + if (_keyboardForegroundConfig.CheckText) + { + // The application needs to validate the response, so we + // submit it to the interactive output buffer, and poll it + // for validation. Once validated, the application will submit + // back a validation status, which is handled in OnInteractiveDataPushIn. + _foregroundState = SoftwareKeyboardState.ValidationPending; + + PushForegroundResponse(true); + } + else + { + // 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. + _foregroundState = SoftwareKeyboardState.Complete; + + PushForegroundResponse(false); + + AppletStateChanged?.Invoke(this, null); + } + } + + private void OnInteractiveData(object sender, EventArgs e) + { + // Obtain the validation status response. + var data = _interactiveSession.Pop(); + + if (_isBackground) + { + lock (_lock) + { + OnBackgroundInteractiveData(data); + } + } + else + { + OnForegroundInteractiveData(data); + } + } + + private void OnForegroundInteractiveData(byte[] data) + { + if (_foregroundState == 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 + // to the standard output buffer and carry on our merry way. + PushForegroundResponse(false); + + AppletStateChanged?.Invoke(this, null); + + _foregroundState = SoftwareKeyboardState.Complete; + } + else if (_foregroundState == SoftwareKeyboardState.Complete) + { + // If we have already completed, we push the result text + // back on the output buffer and poll the application. + PushForegroundResponse(false); + + AppletStateChanged?.Invoke(this, null); + } + else + { + // We shouldn't be able to get here through standard swkbd execution. + throw new InvalidOperationException("Software Keyboard is in an invalid state."); + } + } + + 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; + + Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {_backgroundState}"); + + switch (request) + { + case InlineKeyboardRequest.UseChangedStringV2: + Logger.Stub?.Print(LogClass.ServiceAm, "Inline keyboard request UseChangedStringV2"); + break; + case InlineKeyboardRequest.UseMovedCursorV2: + Logger.Stub?.Print(LogClass.ServiceAm, "Inline keyboard request UseMovedCursorV2"); + break; + case InlineKeyboardRequest.SetUserWordInfo: + // Read the user word info data. + remaining = stream.Length - stream.Position; + if (remaining < sizeof(int)) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes"); + } + else + { + int wordsCount = reader.ReadInt32(); + int wordSize = Unsafe.SizeOf<SoftwareKeyboardUserWord>(); + remaining = stream.Length - stream.Position; + + if (wordsCount > MaxUserWords) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}"); + } + else if (wordsCount * wordSize != remaining) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words"); + } + else + { + _keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount]; + + for (int word = 0; word < wordsCount; word++) + { + _keyboardBackgroundUserWords[word] = reader.ReadStruct<SoftwareKeyboardUserWord>(); + } + } + } + _interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(_backgroundState)); + break; + case InlineKeyboardRequest.SetCustomizeDic: + // Read the custom dic data. + remaining = stream.Length - stream.Position; + if (remaining != Unsafe.SizeOf<SoftwareKeyboardCustomizeDic>()) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes"); + } + else + { + _keyboardBackgroundDic = reader.ReadStruct<SoftwareKeyboardCustomizeDic>(); + } + break; + case InlineKeyboardRequest.SetCustomizedDictionaries: + // Read the custom dictionaries data. + remaining = stream.Length - stream.Position; + if (remaining != Unsafe.SizeOf<SoftwareKeyboardDictSet>()) + { + Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes"); + } + else + { + _keyboardBackgroundDictSet = reader.ReadStruct<SoftwareKeyboardDictSet>(); + } + break; + case InlineKeyboardRequest.Calc: + // The Calc request is used to communicate configuration changes and commands to the keyboard. + // Fields in the Calc struct and operations are masked by the Flags field. + + // Read the Calc data. + SoftwareKeyboardCalcEx newCalc; + remaining = stream.Length - stream.Position; + if (remaining == Marshal.SizeOf<SoftwareKeyboardCalc>()) + { + var keyboardCalcData = reader.ReadBytes((int)remaining); + var keyboardCalc = ReadStruct<SoftwareKeyboardCalc>(keyboardCalcData); + + newCalc = keyboardCalc.ToExtended(); + } + else if (remaining == Marshal.SizeOf<SoftwareKeyboardCalcEx>() || remaining == SoftwareKeyboardCalcEx.AlternativeSize) + { + var keyboardCalcData = reader.ReadBytes((int)remaining); + + newCalc = ReadStruct<SoftwareKeyboardCalcEx>(keyboardCalcData); + } + else + { + Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes"); + + newCalc = new SoftwareKeyboardCalcEx(); + } + + // Process each individual operation specified in the flags. + + bool updateText = false; + + if ((newCalc.Flags & KeyboardCalcFlags.Initialize) != 0) + { + _interactiveSession.Push(InlineResponses.FinishedInitialize(_backgroundState)); + + _backgroundState = InlineKeyboardState.Initialized; + } + + if ((newCalc.Flags & KeyboardCalcFlags.SetCursorPos) != 0) + { + _cursorBegin = newCalc.CursorPos; + updateText = true; + + Logger.Debug?.Print(LogClass.ServiceAm, $"Cursor position set to {_cursorBegin}"); + } + + if ((newCalc.Flags & KeyboardCalcFlags.SetInputText) != 0) + { + _textValue = newCalc.InputText; + updateText = true; + + Logger.Debug?.Print(LogClass.ServiceAm, $"Input text set to {_textValue}"); + } + + if ((newCalc.Flags & KeyboardCalcFlags.SetUtf8Mode) != 0) + { + _encoding = newCalc.UseUtf8 ? Encoding.UTF8 : Encoding.Default; + + Logger.Debug?.Print(LogClass.ServiceAm, $"Encoding set to {_encoding}"); + } + + if (updateText) + { + _dynamicTextInputHandler.SetText(_textValue, _cursorBegin); + _keyboardRenderer.UpdateTextState(_textValue, _cursorBegin, _cursorBegin, null, null); + } + + if ((newCalc.Flags & KeyboardCalcFlags.MustShow) != 0) + { + ActivateFrontend(); + + _backgroundState = InlineKeyboardState.Shown; + + PushChangedString(_textValue, (uint)_cursorBegin, _backgroundState); + } + + // Send the response to the Calc + _interactiveSession.Push(InlineResponses.Default(_backgroundState)); + break; + case InlineKeyboardRequest.Finalize: + // Destroy the frontend. + DestroyFrontend(); + // The calling application wants to close the keyboard applet and will wait for a state change. + _backgroundState = InlineKeyboardState.Uninitialized; + AppletStateChanged?.Invoke(this, null); + break; + default: + // We shouldn't be able to get here through standard swkbd execution. + Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_backgroundState}"); + _interactiveSession.Push(InlineResponses.Default(_backgroundState)); + break; + } + } + } + + private void ActivateFrontend() + { + Logger.Debug?.Print(LogClass.ServiceAm, $"Activating software keyboard frontend"); + + _inputMode = KeyboardInputMode.ControllerAndKeyboard; + + _npads.Update(true); + + NpadButton buttons = _npads.GetCurrentButtonsOfAllNpads(); + + // Block the input if the current accept key is pressed so the applet won't be instantly closed. + _canAcceptController = (buttons & NpadButton.A) == 0; + + _dynamicTextInputHandler.TextProcessingEnabled = true; + + _keyboardRenderer.UpdateCommandState(null, null, true); + _keyboardRenderer.UpdateTextState(null, null, null, null, true); + } + + private void DeactivateFrontend() + { + Logger.Debug?.Print(LogClass.ServiceAm, $"Deactivating software keyboard frontend"); + + _inputMode = KeyboardInputMode.ControllerAndKeyboard; + _canAcceptController = false; + + _dynamicTextInputHandler.TextProcessingEnabled = false; + _dynamicTextInputHandler.SetText(_textValue, _cursorBegin); + } + + private void DestroyFrontend() + { + Logger.Debug?.Print(LogClass.ServiceAm, $"Destroying software keyboard frontend"); + + _keyboardRenderer?.Dispose(); + _keyboardRenderer = null; + + if (_dynamicTextInputHandler != null) + { + _dynamicTextInputHandler.TextChangedEvent -= HandleTextChangedEvent; + _dynamicTextInputHandler.KeyPressedEvent -= HandleKeyPressedEvent; + _dynamicTextInputHandler.Dispose(); + _dynamicTextInputHandler = null; + } + + if (_npads != null) + { + _npads.NpadButtonDownEvent -= HandleNpadButtonDownEvent; + _npads.NpadButtonUpEvent -= HandleNpadButtonUpEvent; + _npads = null; + } + } + + private bool HandleKeyPressedEvent(Key key) + { + if (key == CycleInputModesKey) + { + lock (_lock) + { + if (IsKeyboardActive()) + { + AdvanceInputMode(); + + bool typingEnabled = InputModeTypingEnabled(); + bool controllerEnabled = InputModeControllerEnabled(); + + _dynamicTextInputHandler.TextProcessingEnabled = typingEnabled; + + _keyboardRenderer.UpdateTextState(null, null, null, null, typingEnabled); + _keyboardRenderer.UpdateCommandState(null, null, controllerEnabled); + } + } + } + + return true; + } + + private void HandleTextChangedEvent(string text, int cursorBegin, int cursorEnd, bool overwriteMode) + { + lock (_lock) + { + // Text processing should not run with typing disabled. + Debug.Assert(InputModeTypingEnabled()); + + if (text.Length > MaxUiTextSize) + { + // Limit the text size and change it back. + text = text.Substring(0, MaxUiTextSize); + cursorBegin = Math.Min(cursorBegin, MaxUiTextSize); + cursorEnd = Math.Min(cursorEnd, MaxUiTextSize); + + _dynamicTextInputHandler.SetText(text, cursorBegin, cursorEnd); + } + + _textValue = text; + _cursorBegin = cursorBegin; + _keyboardRenderer.UpdateTextState(text, cursorBegin, cursorEnd, overwriteMode, null); + + PushUpdatedState(text, cursorBegin, KeyboardResult.NotSet); + } + } + + private void HandleNpadButtonDownEvent(int npadIndex, NpadButton button) + { + lock (_lock) + { + if (!IsKeyboardActive()) + { + return; + } + + switch (button) + { + case NpadButton.A: + _keyboardRenderer.UpdateCommandState(_canAcceptController, null, null); + break; + case NpadButton.B: + _keyboardRenderer.UpdateCommandState(null, _canAcceptController, null); + break; + } + } + } + + private void HandleNpadButtonUpEvent(int npadIndex, NpadButton button) + { + lock (_lock) + { + KeyboardResult result = KeyboardResult.NotSet; + + switch (button) + { + case NpadButton.A: + result = KeyboardResult.Accept; + _keyboardRenderer.UpdateCommandState(false, null, null); + break; + case NpadButton.B: + result = KeyboardResult.Cancel; + _keyboardRenderer.UpdateCommandState(null, false, null); + break; + } + + if (IsKeyboardActive()) + { + if (!_canAcceptController) + { + _canAcceptController = true; + } + else if (InputModeControllerEnabled()) + { + PushUpdatedState(_textValue, _cursorBegin, result); + } + } + } + } + + private void PushUpdatedState(string text, int cursorBegin, KeyboardResult result) + { + _lastResult = result; + _textValue = text; + + bool cancel = result == KeyboardResult.Cancel; + bool accept = result == KeyboardResult.Accept; + + if (!IsKeyboardActive()) + { + // Keyboard is not active. + + return; + } + + if (accept == false && cancel == false) + { + Logger.Debug?.Print(LogClass.ServiceAm, $"Updating keyboard text to {text} and cursor position to {cursorBegin}"); + + PushChangedString(text, (uint)cursorBegin, _backgroundState); + } + else + { + // Disable the frontend. + DeactivateFrontend(); + + // The 'Complete' state indicates the Calc request has been fulfilled by the applet. + _backgroundState = InlineKeyboardState.Disappearing; + + if (accept) + { + Logger.Debug?.Print(LogClass.ServiceAm, $"Sending keyboard OK with text {text}"); + + DecidedEnter(text, _backgroundState); + } + else if (cancel) + { + Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel"); + + DecidedCancel(_backgroundState); + } + + _interactiveSession.Push(InlineResponses.Default(_backgroundState)); + + Logger.Debug?.Print(LogClass.ServiceAm, $"Resetting state of the keyboard to {_backgroundState}"); + + // Set the state of the applet to 'Initialized' as it is the only known state so far + // that does not soft-lock the keyboard after use. + + _backgroundState = InlineKeyboardState.Initialized; + + _interactiveSession.Push(InlineResponses.Default(_backgroundState)); + } + } + + private void PushChangedString(string text, uint cursor, InlineKeyboardState state) + { + // TODO (Caian): The *V2 methods are not supported because the applications that request + // them do not seem to accept them. The regular methods seem to work just fine in all cases. + + if (_encoding == Encoding.UTF8) + { + _interactiveSession.Push(InlineResponses.ChangedStringUtf8(text, cursor, state)); + } + else + { + _interactiveSession.Push(InlineResponses.ChangedString(text, cursor, state)); + } + } + + private void DecidedEnter(string text, InlineKeyboardState state) + { + if (_encoding == Encoding.UTF8) + { + _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(text, state)); + } + else + { + _interactiveSession.Push(InlineResponses.DecidedEnter(text, state)); + } + } + + private void DecidedCancel(InlineKeyboardState state) + { + _interactiveSession.Push(InlineResponses.DecidedCancel(state)); + } + + private void PushForegroundResponse(bool interactive) + { + int bufferSize = interactive ? InteractiveBufferSize : StandardBufferSize; + + using (MemoryStream stream = new MemoryStream(new byte[bufferSize])) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + byte[] output = _encoding.GetBytes(_textValue); + + if (!interactive) + { + // Result Code. + writer.Write(_lastResult == KeyboardResult.Accept ? 0U : 1U); + } + else + { + // In interactive mode, we write the length of the text as a long, rather than + // a result code. This field is inclusive of the 64-bit size. + writer.Write((long)output.Length + 8); + } + + writer.Write(output); + + if (!interactive) + { + _normalSession.Push(stream.ToArray()); + } + else + { + _interactiveSession.Push(stream.ToArray()); + } + } + } + + /// <summary> + /// Removes all Unicode control code characters from the input string. + /// This includes CR/LF, tabs, null characters, escape characters, + /// and special control codes which are used for formatting by the real keyboard applet. + /// </summary> + /// <remarks> + /// Some games send special control codes (such as 0x13 "Device Control 3") as part of the string. + /// Future implementations of the emulated keyboard applet will need to handle these as well. + /// </remarks> + /// <param name="input">The input string to sanitize (may be null).</param> + /// <returns>The sanitized string.</returns> + internal static string StripUnicodeControlCodes(string input) + { + if (input is null) + { + return null; + } + + if (input.Length == 0) + { + return string.Empty; + } + + StringBuilder sb = new StringBuilder(capacity: input.Length); + foreach (char c in input) + { + if (!char.IsControl(c)) + { + sb.Append(c); + } + } + + return sb.ToString(); + } + + private static T ReadStruct<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(byte[] data) + where T : struct + { + GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned); + + try + { + return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject()); + } + finally + { + handle.Free(); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs new file mode 100644 index 00000000..90df6fa3 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs @@ -0,0 +1,220 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// A structure with configuration options of the software keyboard when starting a new input request in inline mode. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)] + struct SoftwareKeyboardCalc + { + public const int InputTextLength = SoftwareKeyboardCalcEx.InputTextLength; + + public uint Unknown; + + /// <summary> + /// The size of the Calc struct, as reported by the process communicating with the applet. + /// </summary> + public ushort Size; + + public byte Unknown1; + public byte Unknown2; + + /// <summary> + /// Configuration flags. Each bit in the bitfield enabled a different operation of the keyboard + /// using the data provided with the Calc structure. + /// </summary> + public KeyboardCalcFlags Flags; + + /// <summary> + /// The original parameters used when initializing the keyboard applet. + /// Flag: 0x1 + /// </summary> + public SoftwareKeyboardInitialize Initialize; + + /// <summary> + /// The audio volume used by the sound effects of the keyboard. + /// Flag: 0x2 + /// </summary> + public float Volume; + + /// <summary> + /// The initial position of the text cursor (caret) in the provided input text. + /// Flag: 0x10 + /// </summary> + public int CursorPos; + + /// <summary> + /// Appearance configurations for the on-screen keyboard. + /// </summary> + public SoftwareKeyboardAppear Appear; + + /// <summary> + /// The initial input text to be used by the software keyboard. + /// Flag: 0x8 + /// </summary> + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = InputTextLength + 1)] + public string InputText; + + /// <summary> + /// When set, the strings communicated by software keyboard will be encoded as UTF-8 instead of UTF-16. + /// Flag: 0x20 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool UseUtf8; + + public byte Unknown3; + + /// <summary> + /// [5.0.0+] Enable the backspace key in the software keyboard. + /// Flag: 0x8000 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool BackspaceEnabled; + + public short Unknown4; + public byte Unknown5; + + /// <summary> + /// Flag: 0x200 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool KeytopAsFloating; + + /// <summary> + /// Flag: 0x100 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool FooterScalable; + + /// <summary> + /// Flag: 0x100 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool AlphaEnabledInInputMode; + + /// <summary> + /// Flag: 0x100 + /// </summary> + public byte InputModeFadeType; + + /// <summary> + /// When set, the software keyboard ignores touch input. + /// Flag: 0x200 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool TouchDisabled; + + /// <summary> + /// When set, the software keyboard ignores hardware keyboard commands. + /// Flag: 0x800 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool HardwareKeyboardDisabled; + + public uint Unknown6; + public uint Unknown7; + + /// <summary> + /// Default value is 1.0. + /// Flag: 0x200 + /// </summary> + public float KeytopScale0; + + /// <summary> + /// Default value is 1.0. + /// Flag: 0x200 + /// </summary> + public float KeytopScale1; + + public float KeytopTranslate0; + public float KeytopTranslate1; + + /// <summary> + /// Default value is 1.0. + /// Flag: 0x100 + /// </summary> + public float KeytopBgAlpha; + + /// <summary> + /// Default value is 1.0. + /// Flag: 0x100 + /// </summary> + public float FooterBgAlpha; + + /// <summary> + /// Default value is 1.0. + /// Flag: 0x200 + /// </summary> + public float BalloonScale; + + public float Unknown8; + public uint Unknown9; + public uint Unknown10; + public uint Unknown11; + + /// <summary> + /// [5.0.0+] Enable sound effect. + /// Flag: Enable: 0x2000 + /// Disable: 0x4000 + /// </summary> + public byte SeGroup; + + /// <summary> + /// [6.0.0+] Enables the Trigger field when Trigger is non-zero. + /// </summary> + public byte TriggerFlag; + + /// <summary> + /// [6.0.0+] Always set to zero. + /// </summary> + public byte Trigger; + + public byte Padding; + + public SoftwareKeyboardCalcEx ToExtended() + { + SoftwareKeyboardCalcEx calc = new SoftwareKeyboardCalcEx(); + + calc.Unknown = Unknown; + calc.Size = Size; + calc.Unknown1 = Unknown1; + calc.Unknown2 = Unknown2; + calc.Flags = Flags; + calc.Initialize = Initialize; + calc.Volume = Volume; + calc.CursorPos = CursorPos; + calc.Appear = Appear.ToExtended(); + calc.InputText = InputText; + calc.UseUtf8 = UseUtf8; + calc.Unknown3 = Unknown3; + calc.BackspaceEnabled = BackspaceEnabled; + calc.Unknown4 = Unknown4; + calc.Unknown5 = Unknown5; + calc.KeytopAsFloating = KeytopAsFloating; + calc.FooterScalable = FooterScalable; + calc.AlphaEnabledInInputMode = AlphaEnabledInInputMode; + calc.InputModeFadeType = InputModeFadeType; + calc.TouchDisabled = TouchDisabled; + calc.HardwareKeyboardDisabled = HardwareKeyboardDisabled; + calc.Unknown6 = Unknown6; + calc.Unknown7 = Unknown7; + calc.KeytopScale0 = KeytopScale0; + calc.KeytopScale1 = KeytopScale1; + calc.KeytopTranslate0 = KeytopTranslate0; + calc.KeytopTranslate1 = KeytopTranslate1; + calc.KeytopBgAlpha = KeytopBgAlpha; + calc.FooterBgAlpha = FooterBgAlpha; + calc.BalloonScale = BalloonScale; + calc.Unknown8 = Unknown8; + calc.Unknown9 = Unknown9; + calc.Unknown10 = Unknown10; + calc.Unknown11 = Unknown11; + calc.SeGroup = SeGroup; + calc.TriggerFlag = TriggerFlag; + calc.Trigger = Trigger; + + return calc; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalcEx.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalcEx.cs new file mode 100644 index 00000000..2d3d5dbe --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalcEx.cs @@ -0,0 +1,182 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// A structure with configuration options of the software keyboard when starting a new input request in inline mode. + /// This is the extended version of the structure with extended appear options. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)] + struct SoftwareKeyboardCalcEx + { + /// <summary> + /// This struct was built following Switchbrew's specs, but this size (larger) is also found in real games. + /// It's assumed that this is padding at the end of this struct, because all members seem OK. + /// </summary> + public const int AlternativeSize = 1256; + + public const int InputTextLength = 505; + + public uint Unknown; + + /// <summary> + /// The size of the Calc struct, as reported by the process communicating with the applet. + /// </summary> + public ushort Size; + + public byte Unknown1; + public byte Unknown2; + + /// <summary> + /// Configuration flags. Each bit in the bitfield enabled a different operation of the keyboard + /// using the data provided with the Calc structure. + /// </summary> + public KeyboardCalcFlags Flags; + + /// <summary> + /// The original parameters used when initializing the keyboard applet. + /// Flag: 0x1 + /// </summary> + public SoftwareKeyboardInitialize Initialize; + + /// <summary> + /// The audio volume used by the sound effects of the keyboard. + /// Flag: 0x2 + /// </summary> + public float Volume; + + /// <summary> + /// The initial position of the text cursor (caret) in the provided input text. + /// Flag: 0x10 + /// </summary> + public int CursorPos; + + /// <summary> + /// Appearance configurations for the on-screen keyboard. + /// </summary> + public SoftwareKeyboardAppearEx Appear; + + /// <summary> + /// The initial input text to be used by the software keyboard. + /// Flag: 0x8 + /// </summary> + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = InputTextLength + 1)] + public string InputText; + + /// <summary> + /// When set, the strings communicated by software keyboard will be encoded as UTF-8 instead of UTF-16. + /// Flag: 0x20 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool UseUtf8; + + public byte Unknown3; + + /// <summary> + /// [5.0.0+] Enable the backspace key in the software keyboard. + /// Flag: 0x8000 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool BackspaceEnabled; + + public short Unknown4; + public byte Unknown5; + + /// <summary> + /// Flag: 0x200 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool KeytopAsFloating; + + /// <summary> + /// Flag: 0x100 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool FooterScalable; + + /// <summary> + /// Flag: 0x100 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool AlphaEnabledInInputMode; + + /// <summary> + /// Flag: 0x100 + /// </summary> + public byte InputModeFadeType; + + /// <summary> + /// When set, the software keyboard ignores touch input. + /// Flag: 0x200 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool TouchDisabled; + + /// <summary> + /// When set, the software keyboard ignores hardware keyboard commands. + /// Flag: 0x800 + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool HardwareKeyboardDisabled; + + public uint Unknown6; + public uint Unknown7; + + /// <summary> + /// Default value is 1.0. + /// Flag: 0x200 + /// </summary> + public float KeytopScale0; + + /// <summary> + /// Default value is 1.0. + /// Flag: 0x200 + /// </summary> + public float KeytopScale1; + + public float KeytopTranslate0; + public float KeytopTranslate1; + + /// <summary> + /// Default value is 1.0. + /// Flag: 0x100 + /// </summary> + public float KeytopBgAlpha; + + /// <summary> + /// Default value is 1.0. + /// Flag: 0x100 + /// </summary> + public float FooterBgAlpha; + + /// <summary> + /// Default value is 1.0. + /// Flag: 0x200 + /// </summary> + public float BalloonScale; + + public float Unknown8; + public uint Unknown9; + public uint Unknown10; + public uint Unknown11; + + /// <summary> + /// [5.0.0+] Enable sound effect. + /// Flag: Enable: 0x2000 + /// Disable: 0x4000 + /// </summary> + public byte SeGroup; + + /// <summary> + /// [6.0.0+] Enables the Trigger field when Trigger is non-zero. + /// </summary> + public byte TriggerFlag; + + /// <summary> + /// [6.0.0+] Always set to zero. + /// </summary> + public byte Trigger; + + public byte Padding; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardConfig.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardConfig.cs new file mode 100644 index 00000000..fd462382 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardConfig.cs @@ -0,0 +1,138 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// A structure that defines the configuration options of the software keyboard. + /// </summary> + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + struct SoftwareKeyboardConfig + { + private const int SubmitTextLength = 8; + private const int HeaderTextLength = 64; + private const int SubtitleTextLength = 128; + private const int GuideTextLength = 256; + + /// <summary> + /// Type of keyboard. + /// </summary> + public KeyboardMode Mode; + + /// <summary> + /// The string displayed in the Submit button. + /// </summary> + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = SubmitTextLength + 1)] + public string SubmitText; + + /// <summary> + /// The character displayed in the left button of the numeric keyboard. + /// This is ignored when Mode is not set to NumbersOnly. + /// </summary> + public char LeftOptionalSymbolKey; + + /// <summary> + /// The character displayed in the right button of the numeric keyboard. + /// This is ignored when Mode is not set to NumbersOnly. + /// </summary> + public char RightOptionalSymbolKey; + + /// <summary> + /// When set, predictive typing is enabled making use of the system dictionary, + /// and any custom user dictionary. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool PredictionEnabled; + + /// <summary> + /// Specifies prohibited characters that cannot be input into the text entry area. + /// </summary> + public InvalidCharFlags InvalidCharFlag; + + /// <summary> + /// The initial position of the text cursor displayed in the text entry area. + /// </summary> + public InitialCursorPosition InitialCursorPosition; + + /// <summary> + /// The string displayed in the header area of the keyboard. + /// </summary> + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = HeaderTextLength + 1)] + public string HeaderText; + + /// <summary> + /// The string displayed in the subtitle area of the keyboard. + /// </summary> + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = SubtitleTextLength + 1)] + public string SubtitleText; + + /// <summary> + /// The placeholder string displayed in the text entry area when no text is entered. + /// </summary> + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = GuideTextLength + 1)] + public string GuideText; + + /// <summary> + /// When non-zero, specifies the maximum allowed length of the string entered into the text entry area. + /// </summary> + public int StringLengthMax; + + /// <summary> + /// When non-zero, specifies the minimum allowed length of the string entered into the text entry area. + /// </summary> + public int StringLengthMin; + + /// <summary> + /// When enabled, hides input characters as dots in the text entry area. + /// </summary> + public PasswordMode PasswordMode; + + /// <summary> + /// Specifies whether the text entry area is displayed as a single-line entry, or a multi-line entry field. + /// </summary> + public InputFormMode InputFormMode; + + /// <summary> + /// When set, enables or disables the return key. This value is ignored when single-line entry is specified as the InputFormMode. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool UseNewLine; + + /// <summary> + /// When set, the software keyboard will return a UTF-8 encoded string, rather than UTF-16. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool UseUtf8; + + /// <summary> + /// When set, the software keyboard will blur the game application rendered behind the keyboard. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool UseBlurBackground; + + /// <summary> + /// Offset into the work buffer of the initial text when the keyboard is first displayed. + /// </summary> + public int InitialStringOffset; + + /// <summary> + /// Length of the initial text. + /// </summary> + public int InitialStringLength; + + /// <summary> + /// Offset into the work buffer of the custom user dictionary. + /// </summary> + public int CustomDictionaryOffset; + + /// <summary> + /// Number of entries in the custom user dictionary. + /// </summary> + public int CustomDictionaryCount; + + /// <summary> + /// When set, the text entered will be validated on the application side after the keyboard has been submitted. + /// </summary> + [MarshalAs(UnmanagedType.I1)] + public bool CheckText; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs new file mode 100644 index 00000000..53c8c895 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// A structure used by SetCustomizeDic request to software keyboard. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x70)] + struct SoftwareKeyboardCustomizeDic + { + // Unknown + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs new file mode 100644 index 00000000..38554881 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs @@ -0,0 +1,34 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// A structure with custom dictionary words for the software keyboard. + /// </summary> + [StructLayout(LayoutKind.Sequential, Pack = 2)] + struct SoftwareKeyboardDictSet + { + /// <summary> + /// A 0x1000-byte aligned buffer position. + /// </summary> + public ulong BufferPosition; + + /// <summary> + /// A 0x1000-byte aligned buffer size. + /// </summary> + public uint BufferSize; + + /// <summary> + /// Array of word entries in the buffer. + /// </summary> + public Array24<ulong> Entries; + + /// <summary> + /// Number of used entries in the Entries field. + /// </summary> + public ushort TotalEntries; + + public ushort Padding1; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs new file mode 100644 index 00000000..764d0e38 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs @@ -0,0 +1,26 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// A structure that mirrors the parameters used to initialize the keyboard applet. + /// </summary> + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + struct SoftwareKeyboardInitialize + { + public uint Unknown; + + /// <summary> + /// The applet mode used when launching the swkb. The bits regarding the background vs foreground mode can be wrong. + /// </summary> + public byte LibMode; + + /// <summary> + /// [5.0.0+] Set to 0x1 to indicate a firmware version >= 5.0.0. + /// </summary> + public byte FivePlus; + + public byte Padding1; + public byte Padding2; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs new file mode 100644 index 00000000..c30ad11b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs @@ -0,0 +1,164 @@ +using Ryujinx.HLE.Ui; +using Ryujinx.Memory; +using System; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Class that manages the renderer base class and its state in a multithreaded context. + /// </summary> + internal class SoftwareKeyboardRenderer : IDisposable + { + private const int TextBoxBlinkSleepMilliseconds = 100; + private const int RendererWaitTimeoutMilliseconds = 100; + + private readonly object _stateLock = new object(); + + private SoftwareKeyboardUiState _state = new SoftwareKeyboardUiState(); + private SoftwareKeyboardRendererBase _renderer; + + private TimedAction _textBoxBlinkTimedAction = new TimedAction(); + private TimedAction _renderAction = new TimedAction(); + + public SoftwareKeyboardRenderer(IHostUiTheme uiTheme) + { + _renderer = new SoftwareKeyboardRendererBase(uiTheme); + + StartTextBoxBlinker(_textBoxBlinkTimedAction, _state, _stateLock); + StartRenderer(_renderAction, _renderer, _state, _stateLock); + } + + private static void StartTextBoxBlinker(TimedAction timedAction, SoftwareKeyboardUiState state, object stateLock) + { + timedAction.Reset(() => + { + lock (stateLock) + { + // The blinker is on half of the time and events such as input + // changes can reset the blinker. + state.TextBoxBlinkCounter = (state.TextBoxBlinkCounter + 1) % (2 * SoftwareKeyboardRendererBase.TextBoxBlinkThreshold); + + // Tell the render thread there is something new to render. + Monitor.PulseAll(stateLock); + } + }, TextBoxBlinkSleepMilliseconds); + } + + private static void StartRenderer(TimedAction timedAction, SoftwareKeyboardRendererBase renderer, SoftwareKeyboardUiState state, object stateLock) + { + SoftwareKeyboardUiState internalState = new SoftwareKeyboardUiState(); + + bool canCreateSurface = false; + bool needsUpdate = true; + + timedAction.Reset(() => + { + lock (stateLock) + { + if (!Monitor.Wait(stateLock, RendererWaitTimeoutMilliseconds)) + { + return; + } + + needsUpdate = UpdateStateField(ref state.InputText, ref internalState.InputText); + needsUpdate |= UpdateStateField(ref state.CursorBegin, ref internalState.CursorBegin); + needsUpdate |= UpdateStateField(ref state.CursorEnd, ref internalState.CursorEnd); + needsUpdate |= UpdateStateField(ref state.AcceptPressed, ref internalState.AcceptPressed); + needsUpdate |= UpdateStateField(ref state.CancelPressed, ref internalState.CancelPressed); + needsUpdate |= UpdateStateField(ref state.OverwriteMode, ref internalState.OverwriteMode); + needsUpdate |= UpdateStateField(ref state.TypingEnabled, ref internalState.TypingEnabled); + needsUpdate |= UpdateStateField(ref state.ControllerEnabled, ref internalState.ControllerEnabled); + needsUpdate |= UpdateStateField(ref state.TextBoxBlinkCounter, ref internalState.TextBoxBlinkCounter); + + canCreateSurface = state.SurfaceInfo != null && internalState.SurfaceInfo == null; + + if (canCreateSurface) + { + internalState.SurfaceInfo = state.SurfaceInfo; + } + } + + if (canCreateSurface) + { + renderer.CreateSurface(internalState.SurfaceInfo); + } + + if (needsUpdate) + { + renderer.DrawMutableElements(internalState); + renderer.CopyImageToBuffer(); + needsUpdate = false; + } + }); + } + + private static bool UpdateStateField<T>(ref T source, ref T destination) where T : IEquatable<T> + { + if (!source.Equals(destination)) + { + destination = source; + return true; + } + + return false; + } + +#pragma warning disable CS8632 + public void UpdateTextState(string? inputText, int? cursorBegin, int? cursorEnd, bool? overwriteMode, bool? typingEnabled) +#pragma warning restore CS8632 + { + lock (_stateLock) + { + // Update the parameters that were provided. + _state.InputText = inputText != null ? inputText : _state.InputText; + _state.CursorBegin = cursorBegin.GetValueOrDefault(_state.CursorBegin); + _state.CursorEnd = cursorEnd.GetValueOrDefault(_state.CursorEnd); + _state.OverwriteMode = overwriteMode.GetValueOrDefault(_state.OverwriteMode); + _state.TypingEnabled = typingEnabled.GetValueOrDefault(_state.TypingEnabled); + + // Reset the cursor blink. + _state.TextBoxBlinkCounter = 0; + + // Tell the render thread there is something new to render. + Monitor.PulseAll(_stateLock); + } + } + + public void UpdateCommandState(bool? acceptPressed, bool? cancelPressed, bool? controllerEnabled) + { + lock (_stateLock) + { + // Update the parameters that were provided. + _state.AcceptPressed = acceptPressed.GetValueOrDefault(_state.AcceptPressed); + _state.CancelPressed = cancelPressed.GetValueOrDefault(_state.CancelPressed); + _state.ControllerEnabled = controllerEnabled.GetValueOrDefault(_state.ControllerEnabled); + + // Tell the render thread there is something new to render. + Monitor.PulseAll(_stateLock); + } + } + + public void SetSurfaceInfo(RenderingSurfaceInfo surfaceInfo) + { + lock (_stateLock) + { + _state.SurfaceInfo = surfaceInfo; + + // Tell the render thread there is something new to render. + Monitor.PulseAll(_stateLock); + } + } + + internal bool DrawTo(IVirtualMemoryManager destination, ulong position) + { + return _renderer.WriteBufferToMemory(destination, position); + } + + public void Dispose() + { + _textBoxBlinkTimedAction.RequestCancel(); + _renderAction.RequestCancel(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs new file mode 100644 index 00000000..9a91fa32 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs @@ -0,0 +1,606 @@ +using Ryujinx.HLE.Ui; +using Ryujinx.Memory; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Diagnostics; +using System.IO; +using System.Numerics; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Base class that generates the graphics for the software keyboard applet during inline mode. + /// </summary> + internal class SoftwareKeyboardRendererBase + { + public const int TextBoxBlinkThreshold = 8; + + const string MessageText = "Please use the keyboard to input text"; + const string AcceptText = "Accept"; + const string CancelText = "Cancel"; + const string ControllerToggleText = "Toggle input"; + + private readonly object _bufferLock = new object(); + + private RenderingSurfaceInfo _surfaceInfo = null; + private Image<Argb32> _surface = null; + private byte[] _bufferData = null; + + private Image _ryujinxLogo = null; + private Image _padAcceptIcon = null; + private Image _padCancelIcon = null; + private Image _keyModeIcon = null; + + private float _textBoxOutlineWidth; + private float _padPressedPenWidth; + + private Color _textNormalColor; + private Color _textSelectedColor; + private Color _textOverCursorColor; + + private IBrush _panelBrush; + private IBrush _disabledBrush; + private IBrush _cursorBrush; + private IBrush _selectionBoxBrush; + + private Pen _textBoxOutlinePen; + private Pen _cursorPen; + private Pen _selectionBoxPen; + private Pen _padPressedPen; + + private int _inputTextFontSize; + private Font _messageFont; + private Font _inputTextFont; + private Font _labelsTextFont; + + private RectangleF _panelRectangle; + private Point _logoPosition; + private float _messagePositionY; + + public SoftwareKeyboardRendererBase(IHostUiTheme uiTheme) + { + int ryujinxLogoSize = 32; + + string ryujinxIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Logo_Ryujinx.png"; + _ryujinxLogo = LoadResource(Assembly.GetExecutingAssembly(), ryujinxIconPath, ryujinxLogoSize, ryujinxLogoSize); + + string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png"; + string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png"; + string keyModeIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png"; + + _padAcceptIcon = LoadResource(Assembly.GetExecutingAssembly(), padAcceptIconPath , 0, 0); + _padCancelIcon = LoadResource(Assembly.GetExecutingAssembly(), padCancelIconPath , 0, 0); + _keyModeIcon = LoadResource(Assembly.GetExecutingAssembly(), keyModeIconPath , 0, 0); + + Color panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255); + Color panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150); + Color borderColor = ToColor(uiTheme.DefaultBorderColor); + Color selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor); + + _textNormalColor = ToColor(uiTheme.DefaultForegroundColor); + _textSelectedColor = ToColor(uiTheme.SelectionForegroundColor); + _textOverCursorColor = ToColor(uiTheme.DefaultForegroundColor, null, true); + + float cursorWidth = 2; + + _textBoxOutlineWidth = 2; + _padPressedPenWidth = 2; + + _panelBrush = new SolidBrush(panelColor); + _disabledBrush = new SolidBrush(panelTransparentColor); + _cursorBrush = new SolidBrush(_textNormalColor); + _selectionBoxBrush = new SolidBrush(selectionBackgroundColor); + + _textBoxOutlinePen = new Pen(borderColor, _textBoxOutlineWidth); + _cursorPen = new Pen(_textNormalColor, cursorWidth); + _selectionBoxPen = new Pen(selectionBackgroundColor, cursorWidth); + _padPressedPen = new Pen(borderColor, _padPressedPenWidth); + + _inputTextFontSize = 20; + + CreateFonts(uiTheme.FontFamily); + } + + private void CreateFonts(string uiThemeFontFamily) + { + // Try a list of fonts in case any of them is not available in the system. + + string[] availableFonts = new string[] + { + uiThemeFontFamily, + "Liberation Sans", + "FreeSans", + "DejaVu Sans", + "Lucida Grande" + }; + + foreach (string fontFamily in availableFonts) + { + try + { + _messageFont = SystemFonts.CreateFont(fontFamily, 26, FontStyle.Regular); + _inputTextFont = SystemFonts.CreateFont(fontFamily, _inputTextFontSize, FontStyle.Regular); + _labelsTextFont = SystemFonts.CreateFont(fontFamily, 24, FontStyle.Regular); + + return; + } + catch + { + } + } + + throw new Exception($"None of these fonts were found in the system: {String.Join(", ", availableFonts)}!"); + } + + private Color ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false) + { + var a = (byte)(color.A * 255); + var r = (byte)(color.R * 255); + var g = (byte)(color.G * 255); + var b = (byte)(color.B * 255); + + if (flipRgb) + { + r = (byte)(255 - r); + g = (byte)(255 - g); + b = (byte)(255 - b); + } + + return Color.FromRgba(r, g, b, overrideAlpha.GetValueOrDefault(a)); + } + + private Image LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight) + { + Stream resourceStream = assembly.GetManifestResourceStream(resourcePath); + + return LoadResource(resourceStream, newWidth, newHeight); + } + + private Image LoadResource(Stream resourceStream, int newWidth, int newHeight) + { + Debug.Assert(resourceStream != null); + + var image = Image.Load(resourceStream); + + if (newHeight != 0 && newWidth != 0) + { + image.Mutate(x => x.Resize(newWidth, newHeight, KnownResamplers.Lanczos3)); + } + + return image; + } + + private void SetGraphicsOptions(IImageProcessingContext context) + { + context.GetGraphicsOptions().Antialias = true; + context.GetShapeGraphicsOptions().GraphicsOptions.Antialias = true; + } + + private void DrawImmutableElements() + { + if (_surface == null) + { + return; + } + + _surface.Mutate(context => + { + SetGraphicsOptions(context); + + context.Clear(Color.Transparent); + context.Fill(_panelBrush, _panelRectangle); + context.DrawImage(_ryujinxLogo, _logoPosition, 1); + + float halfWidth = _panelRectangle.Width / 2; + float buttonsY = _panelRectangle.Y + 185; + + PointF disableButtonPosition = new PointF(halfWidth + 180, buttonsY); + + DrawControllerToggle(context, disableButtonPosition); + }); + } + + public void DrawMutableElements(SoftwareKeyboardUiState state) + { + if (_surface == null) + { + return; + } + + _surface.Mutate(context => + { + var messageRectangle = MeasureString(MessageText, _messageFont); + float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.X; + float messagePositionY = _messagePositionY - messageRectangle.Y; + var messagePosition = new PointF(messagePositionX, messagePositionY); + var messageBoundRectangle = new RectangleF(messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height); + + SetGraphicsOptions(context); + + context.Fill(_panelBrush, messageBoundRectangle); + + context.DrawText(MessageText, _messageFont, _textNormalColor, messagePosition); + + if (!state.TypingEnabled) + { + // Just draw a semi-transparent rectangle on top to fade the component with the background. + // TODO (caian): This will not work if one decides to add make background semi-transparent as well. + + context.Fill(_disabledBrush, messageBoundRectangle); + } + + DrawTextBox(context, state); + + float halfWidth = _panelRectangle.Width / 2; + float buttonsY = _panelRectangle.Y + 185; + + PointF acceptButtonPosition = new PointF(halfWidth - 180, buttonsY); + PointF cancelButtonPosition = new PointF(halfWidth , buttonsY); + PointF disableButtonPosition = new PointF(halfWidth + 180, buttonsY); + + DrawPadButton(context, acceptButtonPosition, _padAcceptIcon, AcceptText, state.AcceptPressed, state.ControllerEnabled); + DrawPadButton(context, cancelButtonPosition, _padCancelIcon, CancelText, state.CancelPressed, state.ControllerEnabled); + }); + } + + public void CreateSurface(RenderingSurfaceInfo surfaceInfo) + { + if (_surfaceInfo != null) + { + return; + } + + _surfaceInfo = surfaceInfo; + + Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8); + + // Use the whole area of the image to draw, even the alignment, otherwise it may shear the final + // image if the pitch is different. + uint totalWidth = _surfaceInfo.Pitch / 4; + uint totalHeight = _surfaceInfo.Size / _surfaceInfo.Pitch; + + Debug.Assert(_surfaceInfo.Width <= totalWidth); + Debug.Assert(_surfaceInfo.Height <= totalHeight); + Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size); + + _surface = new Image<Argb32>((int)totalWidth, (int)totalHeight); + + ComputeConstants(); + DrawImmutableElements(); + } + + private void ComputeConstants() + { + int totalWidth = (int)_surfaceInfo.Width; + int totalHeight = (int)_surfaceInfo.Height; + + int panelHeight = 240; + int panelPositionY = totalHeight - panelHeight; + + _panelRectangle = new RectangleF(0, panelPositionY, totalWidth, panelHeight); + + _messagePositionY = panelPositionY + 60; + + int logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2; + int logoPositionY = panelPositionY + 18; + + _logoPosition = new Point(logoPositionX, logoPositionY); + } + private static RectangleF MeasureString(string text, Font font) + { + RendererOptions options = new RendererOptions(font); + + if (text == "") + { + FontRectangle emptyRectangle = TextMeasurer.Measure(" ", options); + + return new RectangleF(0, emptyRectangle.Y, 0, emptyRectangle.Height); + } + + FontRectangle rectangle = TextMeasurer.Measure(text, options); + + return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + } + + private static RectangleF MeasureString(ReadOnlySpan<char> text, Font font) + { + RendererOptions options = new RendererOptions(font); + + if (text == "") + { + FontRectangle emptyRectangle = TextMeasurer.Measure(" ", options); + return new RectangleF(0, emptyRectangle.Y, 0, emptyRectangle.Height); + } + + FontRectangle rectangle = TextMeasurer.Measure(text, options); + + return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + } + + private void DrawTextBox(IImageProcessingContext context, SoftwareKeyboardUiState state) + { + var inputTextRectangle = MeasureString(state.InputText, _inputTextFont); + + float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.X + 8)); + float boxHeight = 32; + float boxY = _panelRectangle.Y + 110; + float boxX = (int)((_panelRectangle.Width - boxWidth) / 2); + + RectangleF boxRectangle = new RectangleF(boxX, boxY, boxWidth, boxHeight); + + RectangleF boundRectangle = new RectangleF(_panelRectangle.X, boxY - _textBoxOutlineWidth, + _panelRectangle.Width, boxHeight + 2 * _textBoxOutlineWidth); + + context.Fill(_panelBrush, boundRectangle); + + context.Draw(_textBoxOutlinePen, boxRectangle); + + float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.X; + float inputTextY = boxY + 5; + + var inputTextPosition = new PointF(inputTextX, inputTextY); + + context.DrawText(state.InputText, _inputTextFont, _textNormalColor, inputTextPosition); + + // Draw the cursor on top of the text and redraw the text with a different color if necessary. + + Color cursorTextColor; + IBrush cursorBrush; + Pen cursorPen; + + float cursorPositionYTop = inputTextY + 1; + float cursorPositionYBottom = cursorPositionYTop + _inputTextFontSize + 1; + float cursorPositionXLeft; + float cursorPositionXRight; + + bool cursorVisible = false; + + if (state.CursorBegin != state.CursorEnd) + { + Debug.Assert(state.InputText.Length > 0); + + cursorTextColor = _textSelectedColor; + cursorBrush = _selectionBoxBrush; + cursorPen = _selectionBoxPen; + + ReadOnlySpan<char> textUntilBegin = state.InputText.AsSpan(0, state.CursorBegin); + ReadOnlySpan<char> textUntilEnd = state.InputText.AsSpan(0, state.CursorEnd); + + var selectionBeginRectangle = MeasureString(textUntilBegin, _inputTextFont); + var selectionEndRectangle = MeasureString(textUntilEnd , _inputTextFont); + + cursorVisible = true; + cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.X; + cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.X; + } + else + { + cursorTextColor = _textOverCursorColor; + cursorBrush = _cursorBrush; + cursorPen = _cursorPen; + + if (state.TextBoxBlinkCounter < TextBoxBlinkThreshold) + { + // Show the blinking cursor. + + int cursorBegin = Math.Min(state.InputText.Length, state.CursorBegin); + ReadOnlySpan<char> textUntilCursor = state.InputText.AsSpan(0, cursorBegin); + var cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont); + + cursorVisible = true; + cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X; + + if (state.OverwriteMode) + { + // The blinking cursor is in overwrite mode so it takes the size of a character. + + if (state.CursorBegin < state.InputText.Length) + { + textUntilCursor = state.InputText.AsSpan(0, cursorBegin + 1); + cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont); + cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X; + } + else + { + cursorPositionXRight = cursorPositionXLeft + _inputTextFontSize / 2; + } + } + else + { + // The blinking cursor is in insert mode so it is only a line. + cursorPositionXRight = cursorPositionXLeft; + } + } + else + { + cursorPositionXLeft = inputTextX; + cursorPositionXRight = inputTextX; + } + } + + if (state.TypingEnabled && cursorVisible) + { + float cursorWidth = cursorPositionXRight - cursorPositionXLeft; + float cursorHeight = cursorPositionYBottom - cursorPositionYTop; + + if (cursorWidth == 0) + { + PointF[] points = new PointF[] + { + new PointF(cursorPositionXLeft, cursorPositionYTop), + new PointF(cursorPositionXLeft, cursorPositionYBottom), + }; + + context.DrawLines(cursorPen, points); + } + else + { + var cursorRectangle = new RectangleF(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight); + + context.Draw(cursorPen , cursorRectangle); + context.Fill(cursorBrush, cursorRectangle); + + Image<Argb32> textOverCursor = new Image<Argb32>((int)cursorRectangle.Width, (int)cursorRectangle.Height); + textOverCursor.Mutate(context => + { + var textRelativePosition = new PointF(inputTextPosition.X - cursorRectangle.X, inputTextPosition.Y - cursorRectangle.Y); + context.DrawText(state.InputText, _inputTextFont, cursorTextColor, textRelativePosition); + }); + + var cursorPosition = new Point((int)cursorRectangle.X, (int)cursorRectangle.Y); + context.DrawImage(textOverCursor, cursorPosition, 1); + } + } + else if (!state.TypingEnabled) + { + // Just draw a semi-transparent rectangle on top to fade the component with the background. + // TODO (caian): This will not work if one decides to add make background semi-transparent as well. + + context.Fill(_disabledBrush, boundRectangle); + } + } + + private void DrawPadButton(IImageProcessingContext context, PointF point, Image icon, string label, bool pressed, bool enabled) + { + // Use relative positions so we can center the the entire drawing later. + + float iconX = 0; + float iconY = 0; + float iconWidth = icon.Width; + float iconHeight = icon.Height; + + var labelRectangle = MeasureString(label, _labelsTextFont); + + float labelPositionX = iconWidth + 8 - labelRectangle.X; + float labelPositionY = 3; + + float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.X; + float fullHeight = iconHeight; + + // Convert all relative positions into absolute. + + float originX = (int)(point.X - fullWidth / 2); + float originY = (int)(point.Y - fullHeight / 2); + + iconX += originX; + iconY += originY; + + var iconPosition = new Point((int)iconX, (int)iconY); + var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY); + + var selectedRectangle = new RectangleF(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth, + fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth); + + var boundRectangle = new RectangleF(originX, originY, fullWidth, fullHeight); + boundRectangle.Inflate(4 * _padPressedPenWidth, 4 * _padPressedPenWidth); + + context.Fill(_panelBrush, boundRectangle); + context.DrawImage(icon, iconPosition, 1); + context.DrawText(label, _labelsTextFont, _textNormalColor, labelPosition); + + if (enabled) + { + if (pressed) + { + context.Draw(_padPressedPen, selectedRectangle); + } + } + else + { + // Just draw a semi-transparent rectangle on top to fade the component with the background. + // TODO (caian): This will not work if one decides to add make background semi-transparent as well. + + context.Fill(_disabledBrush, boundRectangle); + } + } + + private void DrawControllerToggle(IImageProcessingContext context, PointF point) + { + var labelRectangle = MeasureString(ControllerToggleText, _labelsTextFont); + + // Use relative positions so we can center the the entire drawing later. + + float keyWidth = _keyModeIcon.Width; + float keyHeight = _keyModeIcon.Height; + + float labelPositionX = keyWidth + 8 - labelRectangle.X; + float labelPositionY = -labelRectangle.Y - 1; + + float keyX = 0; + float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2); + + float fullWidth = labelPositionX + labelRectangle.Width; + float fullHeight = Math.Max(labelPositionY + labelRectangle.Height, keyHeight); + + // Convert all relative positions into absolute. + + float originX = (int)(point.X - fullWidth / 2); + float originY = (int)(point.Y - fullHeight / 2); + + keyX += originX; + keyY += originY; + + var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY); + var overlayPosition = new Point((int)keyX, (int)keyY); + + context.DrawImage(_keyModeIcon, overlayPosition, 1); + context.DrawText(ControllerToggleText, _labelsTextFont, _textNormalColor, labelPosition); + } + + public void CopyImageToBuffer() + { + lock (_bufferLock) + { + if (_surface == null) + { + return; + } + + // Convert the pixel format used in the image to the one used in the Switch surface. + + if (!_surface.TryGetSinglePixelSpan(out Span<Argb32> pixels)) + { + return; + } + + _bufferData = MemoryMarshal.AsBytes(pixels).ToArray(); + Span<uint> dataConvert = MemoryMarshal.Cast<byte, uint>(_bufferData); + + Debug.Assert(_bufferData.Length == _surfaceInfo.Size); + + for (int i = 0; i < dataConvert.Length; i++) + { + dataConvert[i] = BitOperations.RotateRight(dataConvert[i], 8); + } + } + } + + public bool WriteBufferToMemory(IVirtualMemoryManager destination, ulong position) + { + lock (_bufferLock) + { + if (_bufferData == null) + { + return false; + } + + try + { + destination.Write(position, _bufferData); + } + catch + { + return false; + } + + return true; + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardState.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardState.cs new file mode 100644 index 00000000..0f66fc9b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardState.cs @@ -0,0 +1,28 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Identifies the software keyboard state. + /// </summary> + enum SoftwareKeyboardState + { + /// <summary> + /// swkbd is uninitialized. + /// </summary> + Uninitialized, + + /// <summary> + /// swkbd is ready to process data. + /// </summary> + Ready, + + /// <summary> + /// swkbd is awaiting an interactive reply with a validation status. + /// </summary> + ValidationPending, + + /// <summary> + /// swkbd has completed. + /// </summary> + Complete + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs new file mode 100644 index 00000000..d24adec3 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiArgs.cs @@ -0,0 +1,13 @@ +namespace Ryujinx.HLE.HOS.Applets +{ + public struct SoftwareKeyboardUiArgs + { + public string HeaderText; + public string SubtitleText; + public string InitialText; + public string GuideText; + public string SubmitText; + public int StringLengthMin; + public int StringLengthMax; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs new file mode 100644 index 00000000..e6131e62 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs @@ -0,0 +1,22 @@ +using Ryujinx.HLE.Ui; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// TODO + /// </summary> + internal class SoftwareKeyboardUiState + { + public string InputText = ""; + public int CursorBegin = 0; + public int CursorEnd = 0; + public bool AcceptPressed = false; + public bool CancelPressed = false; + public bool OverwriteMode = false; + public bool TypingEnabled = true; + public bool ControllerEnabled = true; + public int TextBoxBlinkCounter = 0; + + public RenderingSurfaceInfo SurfaceInfo = null; + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs new file mode 100644 index 00000000..f1bfec2b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// A structure used by SetUserWordInfo request to the software keyboard. + /// </summary> + [StructLayout(LayoutKind.Sequential, Size = 0x64)] + struct SoftwareKeyboardUserWord + { + // Unknown + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TRef.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TRef.cs new file mode 100644 index 00000000..53746e74 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TRef.cs @@ -0,0 +1,19 @@ +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Wraps a type in a class so it gets stored in the GC managed heap. This is used as communication mechanism + /// between classed that need to be disposed and, thus, can't share their references. + /// </summary> + /// <typeparam name="T">The internal type.</typeparam> + class TRef<T> + { + public T Value; + + public TRef() { } + + public TRef(T value) + { + Value = value; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs new file mode 100644 index 00000000..0de78a0e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs @@ -0,0 +1,186 @@ +using System; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// A threaded executor of periodic actions that can be cancelled. The total execution time is optional + /// and, in this case, a progress is reported back to the action. + /// </summary> + class TimedAction + { + public const int MaxThreadSleep = 100; + + private class SleepSubstepData + { + public readonly int SleepMilliseconds; + public readonly int SleepCount; + public readonly int SleepRemainderMilliseconds; + + public SleepSubstepData(int sleepMilliseconds) + { + SleepMilliseconds = Math.Min(sleepMilliseconds, MaxThreadSleep); + SleepCount = sleepMilliseconds / SleepMilliseconds; + SleepRemainderMilliseconds = sleepMilliseconds - SleepCount * SleepMilliseconds; + } + } + + private TRef<bool> _cancelled = null; + private Thread _thread = null; + private object _lock = new object(); + + public bool IsRunning + { + get + { + lock (_lock) + { + if (_thread == null) + { + return false; + } + + return _thread.IsAlive; + } + } + } + + public void RequestCancel() + { + lock (_lock) + { + if (_cancelled != null) + { + Volatile.Write(ref _cancelled.Value, true); + } + } + } + + public TimedAction() { } + + private void Reset(Thread thread, TRef<bool> cancelled) + { + lock (_lock) + { + // Cancel the current task. + if (_cancelled != null) + { + Volatile.Write(ref _cancelled.Value, true); + } + + _cancelled = cancelled; + + _thread = thread; + _thread.IsBackground = true; + _thread.Start(); + } + } + + public void Reset(Action<float> action, int totalMilliseconds, int sleepMilliseconds) + { + // Create a dedicated cancel token for each task. + var cancelled = new TRef<bool>(false); + + Reset(new Thread(() => + { + var substepData = new SleepSubstepData(sleepMilliseconds); + + int totalCount = totalMilliseconds / sleepMilliseconds; + int totalRemainder = totalMilliseconds - totalCount * sleepMilliseconds; + + if (Volatile.Read(ref cancelled.Value)) + { + action(-1); + + return; + } + + action(0); + + for (int i = 1; i <= totalCount; i++) + { + if (SleepWithSubstep(substepData, cancelled)) + { + action(-1); + + return; + } + + action((float)(i * sleepMilliseconds) / totalMilliseconds); + } + + if (totalRemainder > 0) + { + if (SleepWithSubstep(substepData, cancelled)) + { + action(-1); + + return; + } + + action(1); + } + }), cancelled); + } + + public void Reset(Action action, int sleepMilliseconds) + { + // Create a dedicated cancel token for each task. + var cancelled = new TRef<bool>(false); + + Reset(new Thread(() => + { + var substepData = new SleepSubstepData(sleepMilliseconds); + + while (!Volatile.Read(ref cancelled.Value)) + { + action(); + + if (SleepWithSubstep(substepData, cancelled)) + { + return; + } + } + }), cancelled); + } + + public void Reset(Action action) + { + // Create a dedicated cancel token for each task. + var cancelled = new TRef<bool>(false); + + Reset(new Thread(() => + { + while (!Volatile.Read(ref cancelled.Value)) + { + action(); + } + }), cancelled); + } + + private static bool SleepWithSubstep(SleepSubstepData substepData, TRef<bool> cancelled) + { + for (int i = 0; i < substepData.SleepCount; i++) + { + if (Volatile.Read(ref cancelled.Value)) + { + return true; + } + + Thread.Sleep(substepData.SleepMilliseconds); + } + + if (substepData.SleepRemainderMilliseconds > 0) + { + if (Volatile.Read(ref cancelled.Value)) + { + return true; + } + + Thread.Sleep(substepData.SleepRemainderMilliseconds); + } + + return Volatile.Read(ref cancelled.Value); + } + } +} |
