aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.HLE/HOS/Services/Caps/CaptureManager.cs
blob: 91a8958e6c20720abdae7118d52c32c67de68859 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Caps.Types;
using SixLabors.ImageSharp;
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 readonly string _sdCardPath;

        private uint _shimLibraryVersion;

        public CaptureManager(Switch device)
        {
            _sdCardPath = FileSystem.VirtualFileSystem.GetSdCardPath();
        }

        public ResultCode SetShimLibraryVersion(ServiceCtx context)
        {
            ulong shimLibraryVersion = context.RequestData.ReadUInt64();
#pragma warning disable IDE0059 // Remove unnecessary value assignment
            ulong appletResourceUserId = context.RequestData.ReadUInt64();
#pragma warning restore IDE0059

            // 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,
                };

                // 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 = Convert.ToHexString(SHA256.HashData(BitConverter.GetBytes(titleId))).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);
        }
    }
}