Ryujinx/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs
Ac_K 32be8caa9d
caps: Implement SaveScreenShot calls and cleanup (#2140)
* caps: Implement SaveScreenShot calls and cleanup

This PR implement:
- caps:u IAlbumApplicationService (32) SetShimLibraryVersion
- caps:c IAlbumControlService (33) SetShimLibraryVersion
- caps:su IScreenShotApplicationService (32) SetShimLibraryVersion
- caps:su IScreenShotApplicationService (203/205/210) SaveScreenShotEx0/SaveScreenShotEx1/SaveScreenShotEx2

ImageSharp is used to save the raw screenshot data as a JPG file following what the service does.
All screenshots are save in: `%AppData%\Ryujinx\sdcard\Nintendo\Album` folder. (as example a screenshot file path will be `%AppData%\Ryujinx\sdcard\Nintendo\Album\2021\03\26\2021032601020300-0123456789ABCDEF0123456789ABCDEF.jpg`

This is needed by Animal Crossing: New Horizon where screenshots looks like this:

And this is needed in Monster Hunter Rise but screenshots are currently empty due to another issue.

* remove useless comment

* Addresses gdkchan feedback

* Addresses gdkchan feedback 2

* remove useless comment 2

* Fix nits
2021-03-26 01:16:08 +01:00

143 lines
No EOL
5.5 KiB
C#

using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Caps.Types;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
namespace Ryujinx.HLE.HOS.Services.Caps
{
class CaptureManager
{
private string _sdCardPath;
private uint _shimLibraryVersion;
public CaptureManager(Switch device)
{
_sdCardPath = device.FileSystem.GetSdCardPath();
SixLabors.ImageSharp.Configuration.Default.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder()
{
Quality = 100
});
}
public ResultCode SetShimLibraryVersion(ServiceCtx context)
{
ulong shimLibraryVersion = context.RequestData.ReadUInt64();
ulong appletResourceUserId = context.RequestData.ReadUInt64();
// TODO: Service checks if the pid is present in an internal list and returns ResultCode.BlacklistedPid if it is.
// The list contents needs to be determined.
ResultCode resultCode = ResultCode.OutOfRange;
if (shimLibraryVersion != 0)
{
if (_shimLibraryVersion == shimLibraryVersion)
{
resultCode = ResultCode.Success;
}
else if (_shimLibraryVersion != 0)
{
resultCode = ResultCode.ShimLibraryVersionAlreadySet;
}
else if (shimLibraryVersion == 1)
{
resultCode = ResultCode.Success;
_shimLibraryVersion = 1;
}
}
return resultCode;
}
public ResultCode SaveScreenShot(byte[] screenshotData, ulong appletResourceUserId, ulong titleId, out ApplicationAlbumEntry applicationAlbumEntry)
{
applicationAlbumEntry = default;
if (screenshotData.Length == 0)
{
return ResultCode.NullInputBuffer;
}
/*
// NOTE: On our current implementation, appletResourceUserId starts at 0, disable it for now.
if (appletResourceUserId == 0)
{
return ResultCode.InvalidArgument;
}
*/
/*
// Doesn't occur in our case.
if (applicationAlbumEntry == null)
{
return ResultCode.NullOutputBuffer;
}
*/
if (screenshotData.Length >= 0x384000)
{
DateTime currentDateTime = DateTime.Now;
applicationAlbumEntry = new ApplicationAlbumEntry()
{
Size = (ulong)Unsafe.SizeOf<ApplicationAlbumEntry>(),
TitleId = titleId,
AlbumFileDateTime = new AlbumFileDateTime()
{
Year = (ushort)currentDateTime.Year,
Month = (byte)currentDateTime.Month,
Day = (byte)currentDateTime.Day,
Hour = (byte)currentDateTime.Hour,
Minute = (byte)currentDateTime.Minute,
Second = (byte)currentDateTime.Second,
UniqueId = 0
},
AlbumStorage = AlbumStorage.Sd,
ContentType = ContentType.Screenshot,
Padding = new Array5<byte>(),
Unknown0x1f = 1
};
using (SHA256 sha256Hash = SHA256.Create())
{
// NOTE: The hex hash is a HMAC-SHA256 (first 32 bytes) using a hardcoded secret key over the titleId, we can simulate it by hashing the titleId instead.
string hash = BitConverter.ToString(sha256Hash.ComputeHash(BitConverter.GetBytes(titleId))).Replace("-", "").Remove(0x20);
string folderPath = Path.Combine(_sdCardPath, "Nintendo", "Album", currentDateTime.Year.ToString("00"), currentDateTime.Month.ToString("00"), currentDateTime.Day.ToString("00"));
string filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash);
// TODO: Handle that using the FS service implementation and return the right error code instead of throwing exceptions.
Directory.CreateDirectory(folderPath);
while (File.Exists(filePath))
{
applicationAlbumEntry.AlbumFileDateTime.UniqueId++;
filePath = GenerateFilePath(folderPath, applicationAlbumEntry, currentDateTime, hash);
}
// NOTE: The saved JPEG file doesn't have the limitation in the extra EXIF data.
Image.LoadPixelData<Rgba32>(screenshotData, 1280, 720).SaveAsJpegAsync(filePath);
}
return ResultCode.Success;
}
return ResultCode.NullInputBuffer;
}
private string GenerateFilePath(string folderPath, ApplicationAlbumEntry applicationAlbumEntry, DateTime currentDateTime, string hash)
{
string fileName = $"{currentDateTime:yyyyMMddHHmmss}{applicationAlbumEntry.AlbumFileDateTime.UniqueId:00}-{hash}.jpg";
return Path.Combine(folderPath, fileName);
}
}
}