diff --git a/Ryujinx.HLE/HOS/Applets/AppletManager.cs b/Ryujinx.HLE/HOS/Applets/AppletManager.cs index 1cba9ec9..5d075882 100644 --- a/Ryujinx.HLE/HOS/Applets/AppletManager.cs +++ b/Ryujinx.HLE/HOS/Applets/AppletManager.cs @@ -1,4 +1,5 @@ -using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using Ryujinx.HLE.HOS.Applets.Browser; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; using System; using System.Collections.Generic; @@ -14,7 +15,10 @@ namespace Ryujinx.HLE.HOS.Applets { { AppletId.PlayerSelect, typeof(PlayerSelectApplet) }, { AppletId.Controller, typeof(ControllerApplet) }, - { AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) } + { AppletId.SoftwareKeyboard, typeof(SoftwareKeyboardApplet) }, + { AppletId.LibAppletWeb, typeof(BrowserApplet) }, + { AppletId.LibAppletShop, typeof(BrowserApplet) }, + { AppletId.LibAppletOff, typeof(BrowserApplet) } }; } diff --git a/Ryujinx.HLE/HOS/Applets/Browser/BootDisplayKind.cs b/Ryujinx.HLE/HOS/Applets/Browser/BootDisplayKind.cs new file mode 100644 index 00000000..fe6e6040 --- /dev/null +++ b/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/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs b/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs new file mode 100644 index 00000000..f9693c34 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs @@ -0,0 +1,105 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Am.AppletAE; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +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 _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(_normalSession.Pop()); + + Logger.PrintStub(LogClass.ServiceAm, $"WebApplet version: 0x{_commonArguments.AppletVersion:x8}"); + + ReadOnlySpan webArguments = _normalSession.Pop(); + + (_shimKind, _arguments) = BrowserArgument.ParseArguments(webArguments); + + Logger.PrintStub(LogClass.ServiceAm, $"Web Arguments: {_arguments.Count}"); + + foreach (BrowserArgument argument in _arguments) + { + Logger.PrintStub(LogClass.ServiceAm, $"{argument.Type}: {argument.GetValue()}"); + } + + if ((_commonArguments.AppletVersion >= 0x80000 && _shimKind == ShimKind.Web) || (_commonArguments.AppletVersion >= 0x30000 && _shimKind == ShimKind.Share)) + { + List result = new List(); + + 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 = new MemoryStream()) + using (BinaryWriter writer = new BinaryWriter(stream)) + { + writer.WriteStruct(result); + + return stream.ToArray(); + } + } + private byte[] BuildResponseNew(List outputArguments) + { + using (MemoryStream stream = new MemoryStream()) + 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/Ryujinx.HLE/HOS/Applets/Browser/BrowserArgument.cs b/Ryujinx.HLE/HOS/Applets/Browser/BrowserArgument.cs new file mode 100644 index 00000000..17fd4089 --- /dev/null +++ b/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 _typeRegistry = new Dictionary + { + { 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) ParseArguments(ReadOnlySpan data) + { + List browserArguments = new List(); + + WebArgHeader header = IApplet.ReadStruct(data.Slice(0, 8)); + + ReadOnlySpan rawTLVs = data.Slice(8); + + for (int i = 0; i < header.Count; i++) + { + WebArgTLV tlv = IApplet.ReadStruct(rawTLVs); + ReadOnlySpan tlvData = rawTLVs.Slice(Unsafe.SizeOf(), tlv.Size); + + browserArguments.Add(new BrowserArgument((WebArgTLVType)tlv.Type, tlvData.ToArray())); + + rawTLVs = rawTLVs.Slice(Unsafe.SizeOf() + 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/Ryujinx.HLE/HOS/Applets/Browser/BrowserOutput.cs b/Ryujinx.HLE/HOS/Applets/Browser/BrowserOutput.cs new file mode 100644 index 00000000..0b368262 --- /dev/null +++ b/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/Ryujinx.HLE/HOS/Applets/Browser/BrowserOutputType.cs b/Ryujinx.HLE/HOS/Applets/Browser/BrowserOutputType.cs new file mode 100644 index 00000000..209ae8ae --- /dev/null +++ b/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/Ryujinx.HLE/HOS/Applets/Browser/DocumentKind.cs b/Ryujinx.HLE/HOS/Applets/Browser/DocumentKind.cs new file mode 100644 index 00000000..385bcdd0 --- /dev/null +++ b/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/Ryujinx.HLE/HOS/Applets/Browser/LeftStickMode.cs b/Ryujinx.HLE/HOS/Applets/Browser/LeftStickMode.cs new file mode 100644 index 00000000..917549d2 --- /dev/null +++ b/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/Ryujinx.HLE/HOS/Applets/Browser/ShimKind.cs b/Ryujinx.HLE/HOS/Applets/Browser/ShimKind.cs new file mode 100644 index 00000000..ca2ef32f --- /dev/null +++ b/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/Ryujinx.HLE/HOS/Applets/Browser/WebArgHeader.cs b/Ryujinx.HLE/HOS/Applets/Browser/WebArgHeader.cs new file mode 100644 index 00000000..c5e19f6c --- /dev/null +++ b/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/Ryujinx.HLE/HOS/Applets/Browser/WebArgTLV.cs b/Ryujinx.HLE/HOS/Applets/Browser/WebArgTLV.cs new file mode 100644 index 00000000..f6c1e5ae --- /dev/null +++ b/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/Ryujinx.HLE/HOS/Applets/Browser/WebArgTLVType.cs b/Ryujinx.HLE/HOS/Applets/Browser/WebArgTLVType.cs new file mode 100644 index 00000000..bd303207 --- /dev/null +++ b/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/Ryujinx.HLE/HOS/Applets/Browser/WebCommonReturnValue.cs b/Ryujinx.HLE/HOS/Applets/Browser/WebCommonReturnValue.cs new file mode 100644 index 00000000..aab7a86b --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/Browser/WebCommonReturnValue.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Applets.Browser +{ + public unsafe struct WebCommonReturnValue + { + public WebExitReason ExitReason; + public uint Padding; + public fixed byte LastUrl[0x1000]; + public ulong LastUrlSize; + } +} diff --git a/Ryujinx.HLE/HOS/Applets/Browser/WebExitReason.cs b/Ryujinx.HLE/HOS/Applets/Browser/WebExitReason.cs new file mode 100644 index 00000000..4e44d34a --- /dev/null +++ b/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/Ryujinx.HLE/HOS/Applets/CommonArguments.cs b/Ryujinx.HLE/HOS/Applets/CommonArguments.cs new file mode 100644 index 00000000..5da34db1 --- /dev/null +++ b/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; + } +}