From 81c6aeec28048db2fcb9ac1c7693014a2c099034 Mon Sep 17 00:00:00 2001 From: Jacobwasbeast Date: Fri, 7 Feb 2025 06:20:12 -0600 Subject: [PATCH] Add the photoViewer applet --- .../ILibraryAppletProxy.cs | 16 + .../ISystemAppletProxy.cs | 2 +- .../ILibraryAppletSelfAccessor.cs | 17 + .../IAppletCommonFunctions.cs | 10 + .../SystemAppletProxy/ICommonStateGetter.cs | 3 + .../IGlobalStateController.cs | 41 ++- .../Services/Caps/IAlbumAccessorService.cs | 309 ++++++++++++++++++ .../HOS/Services/Caps/Types/AlbumEntry.cs | 8 + .../HOS/Services/Caps/Types/AlbumFileId.cs | 16 + .../Caps/Types/ApplicationAlbumFileEntry.cs | 9 + .../Types/LoadAlbumScreenShotImageOutput.cs | 12 + .../Ns/IContentManagementInterface.cs | 38 +++ .../HOS/Services/Ns/IDownloadTaskInterface.cs | 25 ++ ...ReadOnlyApplicationControlDataInterface.cs | 8 + .../Services/Ns/IServiceGetterInterface.cs | 18 + .../Settings/ISystemSettingsServer.cs | 11 + src/Ryujinx/Assets/locales.json | 50 +++ .../UI/Views/Main/MainMenuBarView.axaml | 5 + .../UI/Views/Main/MainMenuBarView.axaml.cs | 11 + 19 files changed, 607 insertions(+), 2 deletions(-) create mode 100644 src/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumEntry.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumFileId.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Caps/Types/ApplicationAlbumFileEntry.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Caps/Types/LoadAlbumScreenShotImageOutput.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ns/IContentManagementInterface.cs create mode 100644 src/Ryujinx.HLE/HOS/Services/Ns/IDownloadTaskInterface.cs diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/ILibraryAppletProxy.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/ILibraryAppletProxy.cs index 7700cac09..04bde3d74 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/ILibraryAppletProxy.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/ILibraryAppletProxy.cs @@ -92,6 +92,22 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService return ResultCode.Success; } + + [CommandCmif(22)] + // GetHomeMenuFunctions() -> object + public ResultCode GetHomeMenuFunctions(ServiceCtx context) + { + MakeObject(context, new IHomeMenuFunctions(context.Device.System)); + return ResultCode.Success; + } + + [CommandCmif(23)] + // GetGlobalStateController() -> object + public ResultCode GetGlobalStateController(ServiceCtx context) + { + MakeObject(context, new IGlobalStateController(context)); + return ResultCode.Success; + } [CommandCmif(1000)] // GetDebugFunctions() -> object diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/ISystemAppletProxy.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/ISystemAppletProxy.cs index dd015fd80..64b2ba7d7 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/ISystemAppletProxy.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/ISystemAppletProxy.cs @@ -78,7 +78,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService // GetGlobalStateController() -> object public ResultCode GetGlobalStateController(ServiceCtx context) { - MakeObject(context, new IGlobalStateController()); + MakeObject(context, new IGlobalStateController(context)); return ResultCode.Success; } diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs index fc02ea172..1b386920e 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/LibraryAppletProxy/ILibraryAppletSelfAccessor.cs @@ -1,5 +1,7 @@ using Ryujinx.Common; +using Ryujinx.HLE.HOS.Applets; using System; +using System.Runtime.InteropServices; namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletProxy { @@ -23,6 +25,21 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib _appletStandalone.InputData.Enqueue(miiEditInputData); } + else if (context.Device.Processes.ActiveApplication.ProgramId == 0x010000000000100D) + { + _appletStandalone = new AppletStandalone() + { + AppletId = AppletId.PhotoViewer, + LibraryAppletMode = LibraryAppletMode.AllForeground, + }; + + CommonArguments arguments = new CommonArguments(); + ReadOnlySpan data = MemoryMarshal.Cast(MemoryMarshal.CreateReadOnlySpan(ref arguments, 1)); + byte[] argumentsBytes = data.ToArray(); + _appletStandalone.InputData.Enqueue(argumentsBytes); + byte[] optionBytes = BitConverter.GetBytes(1); + _appletStandalone.InputData.Enqueue(optionBytes); + } else { throw new NotImplementedException($"{context.Device.Processes.ActiveApplication.ProgramId} applet is not implemented."); diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/IAppletCommonFunctions.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/IAppletCommonFunctions.cs index 13cdd8f11..fcc8f1358 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/IAppletCommonFunctions.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/IAppletCommonFunctions.cs @@ -1,7 +1,17 @@ +using Ryujinx.Common.Logging; + namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemAppletProxy { class IAppletCommonFunctions : IpcService { public IAppletCommonFunctions() { } + + [CommandCmif(70)] + // SetCpuBoostRequestPriority(s32) -> void + public ResultCode SetCpuBoostRequestPriority(ServiceCtx context) + { + Logger.Info?.PrintStub(LogClass.ServiceAm); + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ICommonStateGetter.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ICommonStateGetter.cs index ad776fe6e..3bb703724 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ICommonStateGetter.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/ICommonStateGetter.cs @@ -147,6 +147,9 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys } context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_acquiredSleepLockEventHandle); + // NOTE: This needs to be signaled when sleep lock is acquired so it does not just wait forever. + // However, since we don't support sleep lock yet, it's fine to signal immediately. + _acquiredSleepLockEvent.ReadableEvent.Signal(); Logger.Stub?.PrintStub(LogClass.ServiceAm); diff --git a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/IGlobalStateController.cs b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/IGlobalStateController.cs index 9e46d1cd7..a26140871 100644 --- a/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/IGlobalStateController.cs +++ b/src/Ryujinx.HLE/HOS/Services/Am/AppletAE/AllSystemAppletProxiesService/SystemAppletProxy/IGlobalStateController.cs @@ -1,7 +1,46 @@ +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Horizon.Common; + namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.SystemAppletProxy { class IGlobalStateController : IpcService { - public IGlobalStateController() { } + KEvent _hdcpAuthenticationFailedEvent; + int _hdcpAuthenticationFailedEventHandle; + public IGlobalStateController(ServiceCtx context) + { + _hdcpAuthenticationFailedEvent = new KEvent(context.Device.System.KernelContext); + _hdcpAuthenticationFailedEventHandle = -1; + } + + [CommandCmif(14)] + // ShouldSleepOnBoot() -> u8 + public ResultCode ShouldSleepOnBoot(ServiceCtx context) + { + context.ResponseData.Write(false); + return ResultCode.Success; + } + + [CommandCmif(15)] + // GetHdcpAuthenticationFailedEvent() -> handle + public ResultCode GetHdcpAuthenticationFailedEvent(ServiceCtx context) + { + if (_hdcpAuthenticationFailedEventHandle == -1) + { + Result resultCode = context.Process.HandleTable.GenerateHandle(_hdcpAuthenticationFailedEvent.ReadableEvent, out _hdcpAuthenticationFailedEventHandle); + + if (resultCode != Result.Success) + { + return (ResultCode)resultCode.ErrorCode; + } + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_hdcpAuthenticationFailedEventHandle); + + Logger.Stub?.PrintStub(LogClass.ServiceAm); + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/IAlbumAccessorService.cs b/src/Ryujinx.HLE/HOS/Services/Caps/IAlbumAccessorService.cs index de62b08d7..3e4f46e06 100644 --- a/src/Ryujinx.HLE/HOS/Services/Caps/IAlbumAccessorService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Caps/IAlbumAccessorService.cs @@ -1,8 +1,317 @@ +using LibHac.Common.FixedArrays; +using LibHac.FsSystem; +using Ryujinx.Common; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Caps.Types; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + namespace Ryujinx.HLE.HOS.Services.Caps { [Service("caps:a")] class IAlbumAccessorService : IpcService { + public Dictionary AlbumFiles { get; set; } public IAlbumAccessorService(ServiceCtx context) { } + + [CommandCmif(1)] + [CommandCmif(101)] + // GetAlbumFileList(unknown) -> (unknown<8>, buffer) + public ResultCode GetAlbumFileList(ServiceCtx context) + { + int storageId = context.RequestData.ReadInt32(); + // 0 = Nand or 1 = Sd Card + if (storageId == 1) + { + return ResultCode.Success; + } + Logger.Info?.Print(LogClass.ServiceCaps, $"Initializing album files with storage ID {storageId}."); + Logger.Stub?.PrintStub(LogClass.ServiceCaps); + string path = Path.Combine(AppDataManager.BaseDirPath, "screenshots"); + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + int count = 0; + var buffer = context.Request.ReceiveBuff[0]; + ulong position = buffer.Position; + AlbumFiles = new Dictionary(); + int limit = 10000; + Span entries = stackalloc AlbumEntry[limit]; + Logger.Info?.Print(LogClass.Application, $"Limitting to {entries.Length} photos."); + foreach (string file in System.IO.Directory.GetFiles(path)) + { + if (count+1 >= entries.Length) + { + Logger.Warning?.Print(LogClass.ServiceCaps,$"Too many screenshots. Limiting to {entries.Length}."); + break; + } + if (System.IO.Path.GetFileName(file).EndsWith(".png") || System.IO.Path.GetFileName(file).EndsWith(".jpg")) + { + Logger.Stub?.Print(LogClass.Application, $"Adding screenshot {System.IO.Path.GetFileName(file)}"); + AlbumEntry album_entry = new AlbumEntry(); + album_entry.EntrySize = (ulong) System.IO.Path.GetFileName(file).Length; + album_entry.FileId = new AlbumFileId(); + album_entry.FileId.ApplicationId = 0x0; + album_entry.FileId.Time = FromDateTime(System.IO.File.GetLastWriteTimeUtc(file), (byte)count); + if (AlbumFiles.ContainsKey(album_entry.FileId.Time)) + { + Logger.Warning?.Print(LogClass.ServiceCaps,$"Duplicate photo found {System.IO.Path.GetFileName(file)}. Skipping."); + continue; + } + album_entry.FileId.Storage = (byte)AlbumStorage.Sd; + album_entry.FileId.Contents = 0; + album_entry.FileId.Field19_0 = 0; + album_entry.FileId.Field19_1 = 0; + album_entry.FileId.Reserved = 0; + entries[count] = album_entry; + count++; + AlbumFiles.Add(album_entry.FileId.Time, file); + } + } + byte[] entryArray = MemoryMarshal.Cast(MemoryMarshal.CreateReadOnlySpan(ref entries[0], count)).ToArray(); + context.Memory.Write(buffer.Position, entryArray); + Logger.Info?.Print(LogClass.ServiceCaps, $"GetAlbumFileCount(): returning {count}"); + context.ResponseData.Write(count); + return ResultCode.Success; + } + + [CommandCmif(2)] + // LoadAlbumScreenShotImage(unknown<0x18>) -> (unknown<8>, buffer) + public ResultCode LoadAlbumScreenShotImage(ServiceCtx context) + { + var fileId = context.RequestData.ReadStruct(); + return LoadImage(1280, 720, context, fileId); + } + + [CommandCmif(5)] + // IsAlbumMounted() -> bool + public ResultCode IsAlbumMounted(ServiceCtx context) + { + // TODO: Implement this properly. + Logger.Stub?.PrintStub(LogClass.ServiceCaps); + context.ResponseData.Write(true); + return ResultCode.Success; + } + + [CommandCmif(8)] + // LoadAlbumScreenShotImageEx0 + public ResultCode LoadAlbumScreenShotImageEx0(ServiceCtx context) + { + var fileId = context.RequestData.ReadStruct(); + return LoadImage(1280, 720, context, fileId); + } + + [CommandCmif(14)] + public ResultCode LoadAlbumScreenShotThumbnail(ServiceCtx context) + { + var fileId = context.RequestData.ReadStruct(); + return LoadImage(320, 180, context, fileId); + } + + public ResultCode LoadImageEx(int width, int height, ServiceCtx context, AlbumFileId fileId) + { + var outputBuffer = context.Request.ReceiveBuff[0]; + var inputBuffer = context.Request.ReceiveBuff[1]; + + var output = new LoadAlbumScreenShotImageOutput + { + width = width, + height = height, + attribute = new ScreenShotAttribute{ + Unknown0x00 = {}, + AlbumImageOrientation = AlbumImageOrientation.Degrees0, + Unknown0x08 = {}, + Unknown0x10 = {} + } + }; + + string imagePath = AlbumFiles[fileId.Time]; + + ScaleBytes(width, height, imagePath, out Span scaledBytes); + + ReadOnlySpan data = MemoryMarshal.Cast(MemoryMarshal.CreateReadOnlySpan(ref output, 1)); + byte[] outputBytes = data.ToArray(); + + context.Memory.Write(outputBuffer.Position, outputBytes); + context.Memory.Write(inputBuffer.Position, scaledBytes); + return ResultCode.Success; + } + + public ResultCode LoadImage(int width, int height, ServiceCtx context, AlbumFileId fileId) + { + var outputBuffer = context.Request.ReceiveBuff[0]; + + var output = new LoadAlbumScreenShotImageOutput + { + width = width, + height = height, + attribute = new ScreenShotAttribute{ + Unknown0x00 = {}, + AlbumImageOrientation = AlbumImageOrientation.Degrees0, + Unknown0x08 = {}, + Unknown0x10 = {} + } + }; + + string imagePath = AlbumFiles[fileId.Time]; + + ScaleBytes(width, height, imagePath, out Span scaledBytes); + + ReadOnlySpan data = MemoryMarshal.Cast(MemoryMarshal.CreateReadOnlySpan(ref output, 1)); + byte[] outputBytes = data.ToArray(); + + context.ResponseData.Write(outputBytes); + context.Memory.Write(outputBuffer.Position, scaledBytes); + return ResultCode.Success; + } + + public ResultCode LoadImageEx1(int width, int height, ServiceCtx context, AlbumFileId fileId) + { + var outputImageSettings = context.Request.ReceiveBuff[0]; + var outputData = context.Request.ReceiveBuff[1]; + var buff3 = context.Request.ReceiveBuff[2]; + Logger.Info?.Print(LogClass.ServiceCaps, $"Loading thumbnail for {fileId.Time.UniqueId}"); + + var output = new LoadAlbumScreenShotImageOutput + { + width = width, + height = height, + attribute = new ScreenShotAttribute{ + Unknown0x00 = {}, + AlbumImageOrientation = AlbumImageOrientation.Degrees0, + Unknown0x08 = {}, + Unknown0x10 = {} + } + }; + + string imagePath = AlbumFiles[fileId.Time]; + + ScaleBytes(width, height, imagePath, out Span scaledBytes); + + ReadOnlySpan data = MemoryMarshal.Cast(MemoryMarshal.CreateReadOnlySpan(ref output, 1)); + byte[] outputBytes = data.ToArray(); + + context.Memory.Write(outputImageSettings.Position, outputBytes); + context.Memory.Write(outputData.Position, scaledBytes); + + return ResultCode.Success; + } + + public void ScaleBytes(int width, int height, string imagePath, out Span output_bytes) + { + using (SKBitmap bitmap = SKBitmap.Decode(imagePath)) + { + if (bitmap == null) + throw new ArgumentException("Unable to decode the input image."); + + // STBI_rgb_alpha + SKImageInfo targetInfo = new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); + using (SKBitmap scaledBitmap = new SKBitmap(targetInfo)) + { + using (SKCanvas canvas = new SKCanvas(scaledBitmap)) + { + canvas.Clear(SKColors.Transparent); // Clear canvas to avoid artifacts + + var paint = new SKPaint + { + FilterQuality = SKFilterQuality.High, // High-quality scaling + IsAntialias = true, // Smooth edges + }; + + // Draw the scaled image + canvas.DrawBitmap(bitmap, new SKRect(0, 0, width, height), paint); + } + + output_bytes = scaledBitmap.Bytes; + } + } + } + + [CommandCmif(18)] + // GetAppletProgramIdTable(buffer) -> bool + public ResultCode GetAppletProgramIdTable(ServiceCtx context) + { + ulong tableBufPos = context.Request.ReceiveBuff[0].Position; + ulong tableBufSize = context.Request.ReceiveBuff[0].Size; + + if (tableBufPos == 0) + { + return ResultCode.NullOutputBuffer; + } + + context.Memory.Write(tableBufPos, 0x0100000000001000UL); + context.Memory.Write(tableBufPos + 8, 0x0100000000001fffUL); + + context.ResponseData.Write(true); + + return ResultCode.Success; + } + + [CommandCmif(401)] + // GetAutoSavingStorage() -> bool + public ResultCode GetAutoSavingStorage(ServiceCtx context) + { + // TODO: Implement this properly. + Logger.Stub?.PrintStub(LogClass.ServiceCaps); + context.ResponseData.Write(false); + return ResultCode.Success; + } + + [CommandCmif(1001)] + // LoadAlbumScreenShotThumbnailImageEx0(unknown<0x38>) -> (unknown<0x50>, buffer, buffer) + public ResultCode LoadAlbumScreenShotThumbnailImageEx0(ServiceCtx context) + { + var fileId = context.RequestData.ReadStruct(); + return LoadImageEx(320, 180, context, fileId); + } + + [CommandCmif(1002)] + // LoadAlbumScreenShotImageEx1(unknown<0x38>) -> (buffer, buffer, buffer) + public ResultCode LoadAlbumScreenShotImageEx1(ServiceCtx context) + { + var fileId = context.RequestData.ReadStruct(); + + return LoadImageEx1(1280, 720, context, fileId); + } + + [CommandCmif(1003)] + public ResultCode LoadAlbumScreenShotThumbnailImageEx1(ServiceCtx context) + { + var fileId = context.RequestData.ReadStruct(); + return LoadImageEx1(320, 180, context, fileId); + } + + public void GetWidthAndHeightFromInputBuffer(AlbumFileId id, out int width, out int height) + { + string path = AlbumFiles[id.Time]; + width = 0; + height = 0; + using (SKBitmap bitmap = SKBitmap.Decode(path)) + { + if (bitmap != null) + { + width = bitmap.Width; + height = bitmap.Height; + } + } + } + + public static AlbumFileDateTime FromDateTime(DateTime dateTime, byte id) + { + return new AlbumFileDateTime + { + Year = (ushort)dateTime.Year, + Month = (byte)dateTime.Month, + Day = (byte)dateTime.Day, + Hour = (byte)dateTime.Hour, + Minute = (byte)dateTime.Minute, + Second = (byte)dateTime.Second, + UniqueId = id + }; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumEntry.cs b/src/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumEntry.cs new file mode 100644 index 000000000..616b32e9c --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumEntry.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Caps.Types +{ + struct AlbumEntry + { + public ulong EntrySize; + public AlbumFileId FileId; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumFileId.cs b/src/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumFileId.cs new file mode 100644 index 000000000..496f75bd1 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Caps/Types/AlbumFileId.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Caps.Types +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct AlbumFileId + { + public ulong ApplicationId; + public AlbumFileDateTime Time; + public byte Storage; + public byte Contents; + public byte Field19_0; + public byte Field19_1; + public uint Reserved; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/Types/ApplicationAlbumFileEntry.cs b/src/Ryujinx.HLE/HOS/Services/Caps/Types/ApplicationAlbumFileEntry.cs new file mode 100644 index 000000000..c56c59213 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Caps/Types/ApplicationAlbumFileEntry.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.HOS.Services.Caps.Types +{ + struct ApplicationAlbumFileEntry + { + public ApplicationAlbumEntry ApplicationAlbumEntry; + public AlbumFileDateTime Date; + public Common.Memory.Array8 Unknown; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Caps/Types/LoadAlbumScreenShotImageOutput.cs b/src/Ryujinx.HLE/HOS/Services/Caps/Types/LoadAlbumScreenShotImageOutput.cs new file mode 100644 index 000000000..6265d40ad --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Caps/Types/LoadAlbumScreenShotImageOutput.cs @@ -0,0 +1,12 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Caps.Types +{ + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x500)] + struct LoadAlbumScreenShotImageOutput + { + public long width; + public long height; + public ScreenShotAttribute attribute; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ns/IContentManagementInterface.cs b/src/Ryujinx.HLE/HOS/Services/Ns/IContentManagementInterface.cs new file mode 100644 index 000000000..aa5929b34 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ns/IContentManagementInterface.cs @@ -0,0 +1,38 @@ +using Ryujinx.Common.Logging; + +namespace Ryujinx.HLE.HOS.Services.Ns +{ + class IContentManagementInterface : IpcService + { + public IContentManagementInterface(ServiceCtx context) { } + + [CommandCmif(43)] + // CheckSdCardMountStatus() + public ResultCode CheckSdCardMountStatus(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceNs); + return ResultCode.Success; + } + + // TODO: Implement proper space size calculation. + const long storageFreeAndTotalSpaceSize = 6999999999999L; + [CommandCmif(47)] + // GetTotalSpaceSize(u8 storage_id) -> u64 + public ResultCode GetTotalSpaceSize(ServiceCtx context) + { + long storageId = context.RequestData.ReadByte(); + context.ResponseData.Write(storageFreeAndTotalSpaceSize); + return ResultCode.Success; + } + + [CommandCmif(48)] + // GetFreeSpaceSize(u8 storage_id) -> u64 + public ResultCode GetFreeSpaceSize(ServiceCtx context) + { + long storageId = context.RequestData.ReadByte(); + context.ResponseData.Write(storageFreeAndTotalSpaceSize); + return ResultCode.Success; + } + + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ns/IDownloadTaskInterface.cs b/src/Ryujinx.HLE/HOS/Services/Ns/IDownloadTaskInterface.cs new file mode 100644 index 000000000..caa82fc58 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Ns/IDownloadTaskInterface.cs @@ -0,0 +1,25 @@ +using Ryujinx.Common.Logging; + +namespace Ryujinx.HLE.HOS.Services.Ns +{ + class IDownloadTaskInterface : IpcService + { + public IDownloadTaskInterface(ServiceCtx context) { } + + [CommandCmif(707)] + // EnableAutoCommit() + public ResultCode EnableAutoCommit(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceNs); + return ResultCode.Success; + } + + [CommandCmif(708)] + // DisableAutoCommit() + public ResultCode DisableAutoCommit(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceNs); + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Ns/IReadOnlyApplicationControlDataInterface.cs b/src/Ryujinx.HLE/HOS/Services/Ns/IReadOnlyApplicationControlDataInterface.cs index ca7d42b48..73a164135 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ns/IReadOnlyApplicationControlDataInterface.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ns/IReadOnlyApplicationControlDataInterface.cs @@ -24,5 +24,13 @@ namespace Ryujinx.HLE.HOS.Services.Ns return ResultCode.Success; } + + [CommandCmif(1)] + // GetApplicationDesiredLanguage() -> u32 + public ResultCode GetApplicationDesiredLanguage(ServiceCtx context) + { + context.ResponseData.Write((uint)context.Device.Configuration.Region); + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Ns/IServiceGetterInterface.cs b/src/Ryujinx.HLE/HOS/Services/Ns/IServiceGetterInterface.cs index e45c6750c..d4af1afa6 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ns/IServiceGetterInterface.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ns/IServiceGetterInterface.cs @@ -26,5 +26,23 @@ namespace Ryujinx.HLE.HOS.Services.Ns return ResultCode.Success; } + + [CommandCmif(7997)] + // GetDownloadTaskInterface() -> object + public ResultCode GetDownloadTaskInterface(ServiceCtx context) + { + MakeObject(context, new IDownloadTaskInterface(context)); + + return ResultCode.Success; + } + + [CommandCmif(7998)] + // GetContentManagementInterface() -> object + public ResultCode GetContentManagementInterface(ServiceCtx context) + { + MakeObject(context, new IContentManagementInterface(context)); + + return ResultCode.Success; + } } } diff --git a/src/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs b/src/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs index cfad2884a..4ad57bedc 100644 --- a/src/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs +++ b/src/Ryujinx.HLE/HOS/Services/Settings/ISystemSettingsServer.cs @@ -290,6 +290,17 @@ namespace Ryujinx.HLE.HOS.Services.Settings return ResultCode.Success; } + + [CommandCmif(79)] + // GetProductModel() -> s32 + public ResultCode GetProductModel(ServiceCtx context) + { + context.ResponseData.Write(1); + + Logger.Stub?.PrintStub(LogClass.ServiceSet); + + return ResultCode.Success; + } [CommandCmif(90)] // GetMiiAuthorId() -> nn::util::Uuid diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 5afb46c13..e89b872b5 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -122,6 +122,56 @@ "zh_TW": "在獨立模式下開啟 Mii 編輯器小程式" } }, + { + "ID": "MenuBarFileOpenAppletOpenPhotoViewerApplet", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "PhotoViewer Applet", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "MenuBarFileOpenAppletOpenPhotoViewerAppletToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Open PhotoViewer Applet in Standalone mode", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "SettingsTabInputDirectMouseAccess", "Translations": { diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index cacb4b130..24d71b495 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -60,6 +60,11 @@ Header="{ext:Locale MenuBarFileOpenAppletOpenMiiApplet}" Icon="{ext:Icon fa-solid fa-person}" ToolTip.Tip="{ext:Locale MenuBarFileOpenAppletOpenMiiAppletToolTip}" /> + ViewModel.AppHost?.Pause()); @@ -154,6 +155,16 @@ namespace Ryujinx.Ava.UI.Views.Main await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData); } + + public AppletMetadata PhotoViewer => new(ViewModel.ContentManager,"photoViewer", 0x010000000000100D); + + public async Task OpenPhotoViewerApplet() + { + if (PhotoViewer.CanStart(ViewModel.ContentManager, out var appData, out var nacpData)) + { + await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData); + } + } public async Task OpenCheatManagerForCurrentApp() {