From e485ee049d8d9418f3bdceed8d3342cc09977f50 Mon Sep 17 00:00:00 2001 From: emmauss Date: Sun, 12 Jan 2020 02:10:55 +0000 Subject: [PATCH] System firmware installer (#791) * firmware installer * Add directory installation option and fix 9.x support for directory * Fix missing system font error while installing for the first time * Address code style comments * Create and use InvalidFirmwarePackageException * Fix LDj3SNuD's comments * addressed alex's comments * add label to status bar to show current firmware version Co-authored-by: Thog --- .../InvalidFirmwarePackageException.cs | 9 + .../FileSystem/Content/ContentManager.cs | 573 +++++++++++++++++- .../FileSystem/Content/SystemVersion.cs | 41 ++ Ryujinx.HLE/HOS/Font/SharedFontManager.cs | 20 +- Ryujinx.HLE/HOS/Horizon.cs | 15 + .../HOS/Services/Sdb/Pl/ISharedFontManager.cs | 2 +- Ryujinx/Ui/MainWindow.cs | 202 +++++- Ryujinx/Ui/MainWindow.glade | 101 ++- 8 files changed, 936 insertions(+), 27 deletions(-) create mode 100644 Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs create mode 100644 Ryujinx.HLE/FileSystem/Content/SystemVersion.cs diff --git a/Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs b/Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs new file mode 100644 index 0000000000..bddd827a01 --- /dev/null +++ b/Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs @@ -0,0 +1,9 @@ +using System; + +namespace Ryujinx.HLE.Exceptions +{ + class InvalidFirmwarePackageException : Exception + { + public InvalidFirmwarePackageException(string message) : base(message) { } + } +} diff --git a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs index 17faa19f62..b83ae44064 100644 --- a/Ryujinx.HLE/FileSystem/Content/ContentManager.cs +++ b/Ryujinx.HLE/FileSystem/Content/ContentManager.cs @@ -1,22 +1,30 @@ -using LibHac.FsSystem; +using LibHac; +using LibHac.Fs; +using LibHac.FsSystem; using LibHac.FsSystem.NcaUtils; +using LibHac.Ncm; +using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.HOS.Services.Time; using Ryujinx.HLE.Utilities; using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; namespace Ryujinx.HLE.FileSystem.Content { internal class ContentManager { + private const ulong SystemVersionTitleId = 0x0100000000000809; + private const ulong SystemUpdateTitleId = 0x0100000000000816; + private Dictionary> _locationEntries; - private Dictionary _sharedFontTitleDictionary; + private Dictionary _sharedFontTitleDictionary; private Dictionary _sharedFontFilenameDictionary; - private SortedDictionary<(ulong, NcaContentType), string> _contentDictionary; + private SortedDictionary<(ulong titleId, NcaContentType type), string> _contentDictionary; private Switch _device; @@ -48,9 +56,10 @@ namespace Ryujinx.HLE.FileSystem.Content _device = device; } - public void LoadEntries() + public void LoadEntries(bool ignoreMissingFonts = false) { _contentDictionary = new SortedDictionary<(ulong, NcaContentType), string>(); + _locationEntries = new Dictionary>(); foreach (StorageId storageId in Enum.GetValues(typeof(StorageId))) { @@ -144,6 +153,8 @@ namespace Ryujinx.HLE.FileSystem.Content } TimeManager.Instance.InitializeTimeZone(_device); + + _device.System.Font.Initialize(this, ignoreMissingFonts); } public void ClearEntry(long titleId, NcaContentType contentType, StorageId storageId) @@ -153,7 +164,7 @@ namespace Ryujinx.HLE.FileSystem.Content public void RefreshEntries(StorageId storageId, int flag) { - LinkedList locationList = _locationEntries[storageId]; + LinkedList locationList = _locationEntries[storageId]; LinkedListNode locationEntry = locationList.First; while (locationEntry != null) @@ -173,9 +184,10 @@ namespace Ryujinx.HLE.FileSystem.Content { if (_contentDictionary.ContainsValue(ncaId)) { - var content = _contentDictionary.FirstOrDefault(x => x.Value == ncaId); - long titleId = (long)content.Key.Item1; - NcaContentType contentType = content.Key.Item2; + var content = _contentDictionary.FirstOrDefault(x => x.Value == ncaId); + long titleId = (long)content.Key.Item1; + + NcaContentType contentType = content.Key.type; StorageId storage = GetInstalledStorage(titleId, contentType, storageId); return storage == storageId; @@ -186,9 +198,9 @@ namespace Ryujinx.HLE.FileSystem.Content public UInt128 GetInstalledNcaId(long titleId, NcaContentType contentType) { - if (_contentDictionary.ContainsKey(((ulong)titleId,contentType))) + if (_contentDictionary.ContainsKey(((ulong)titleId, contentType))) { - return new UInt128(_contentDictionary[((ulong)titleId,contentType)]); + return new UInt128(_contentDictionary[((ulong)titleId, contentType)]); } return new UInt128(); @@ -232,9 +244,8 @@ namespace Ryujinx.HLE.FileSystem.Content { return false; } - - StorageId storageId = LocationHelper.GetStorageId(locationEntry.ContentPath); - string installedPath = _device.FileSystem.SwitchPathToSystemPath(locationEntry.ContentPath); + + string installedPath = _device.FileSystem.SwitchPathToSystemPath(locationEntry.ContentPath); if (!string.IsNullOrWhiteSpace(installedPath)) { @@ -242,7 +253,7 @@ namespace Ryujinx.HLE.FileSystem.Content { using (FileStream file = new FileStream(installedPath, FileMode.Open, FileAccess.Read)) { - Nca nca = new Nca(_device.System.KeySet, file.AsStorage()); + Nca nca = new Nca(_device.System.KeySet, file.AsStorage()); bool contentCheck = nca.Header.ContentType == contentType; return contentCheck; @@ -310,5 +321,539 @@ namespace Ryujinx.HLE.FileSystem.Content return locationList.ToList().Find(x => x.TitleId == titleId && x.ContentType == contentType); } + + public void InstallFirmware(string firmwareSource) + { + string contentPathString = LocationHelper.GetContentRoot(StorageId.NandSystem); + string contentDirectory = LocationHelper.GetRealPath(_device.FileSystem, contentPathString); + string registeredDirectory = Path.Combine(contentDirectory, "registered"); + string temporaryDirectory = Path.Combine(contentDirectory, "temp"); + + if (Directory.Exists(temporaryDirectory)) + { + Directory.Delete(temporaryDirectory, true); + } + + if (Directory.Exists(firmwareSource)) + { + InstallFromDirectory(firmwareSource, temporaryDirectory); + FinishInstallation(temporaryDirectory, registeredDirectory); + + return; + } + + if (!File.Exists(firmwareSource)) + { + throw new FileNotFoundException("Firmware file does not exist."); + } + + FileInfo info = new FileInfo(firmwareSource); + + using (FileStream file = File.OpenRead(firmwareSource)) + { + switch (info.Extension) + { + case ".zip": + using (ZipArchive archive = ZipFile.OpenRead(firmwareSource)) + { + InstallFromZip(archive, temporaryDirectory); + } + break; + case ".xci": + Xci xci = new Xci(_device.System.KeySet, file.AsStorage()); + InstallFromCart(xci, temporaryDirectory); + break; + default: + throw new InvalidFirmwarePackageException("Input file is not a valid firmware package"); + } + + FinishInstallation(temporaryDirectory, registeredDirectory); + } + } + + private void FinishInstallation(string temporaryDirectory, string registeredDirectory) + { + if (Directory.Exists(registeredDirectory)) + { + new DirectoryInfo(registeredDirectory).Delete(true); + } + + Directory.Move(temporaryDirectory, registeredDirectory); + + LoadEntries(); + } + + private void InstallFromDirectory(string firmwareDirectory, string temporaryDirectory) + { + InstallFromPartition(new LocalFileSystem(firmwareDirectory), temporaryDirectory); + } + + private void InstallFromPartition(IFileSystem filesystem, string temporaryDirectory) + { + foreach (var entry in filesystem.EnumerateEntries("/", "*.nca")) + { + Nca nca = new Nca(_device.System.KeySet, OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage()); + + SaveNca(nca, entry.Name.Remove(entry.Name.IndexOf('.')), temporaryDirectory); + } + } + + private void InstallFromCart(Xci gameCard, string temporaryDirectory) + { + if (gameCard.HasPartition(XciPartitionType.Update)) + { + XciPartition partition = gameCard.OpenPartition(XciPartitionType.Update); + + InstallFromPartition(partition, temporaryDirectory); + } + else + { + throw new Exception("Update not found in xci file."); + } + } + + private void InstallFromZip(ZipArchive archive, string temporaryDirectory) + { + using (archive) + { + foreach (var entry in archive.Entries) + { + if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00")) + { + // Clean up the name and get the NcaId + + string[] pathComponents = entry.FullName.Replace(".cnmt", "").Split('/'); + + string ncaId = pathComponents[pathComponents.Length - 1]; + + // If this is a fragmented nca, we need to get the previous element.GetZip + if (ncaId.Equals("00")) + { + ncaId = pathComponents[pathComponents.Length - 2]; + } + + if (ncaId.Contains(".nca")) + { + string newPath = Path.Combine(temporaryDirectory, ncaId); + + Directory.CreateDirectory(newPath); + + entry.ExtractToFile(Path.Combine(newPath, "00")); + } + } + } + } + } + + public void SaveNca(Nca nca, string ncaId, string temporaryDirectory) + { + string newPath = Path.Combine(temporaryDirectory, ncaId + ".nca"); + + Directory.CreateDirectory(newPath); + + using (FileStream file = File.Create(Path.Combine(newPath, "00"))) + { + nca.BaseStorage.AsStream().CopyTo(file); + } + } + + private IFile OpenPossibleFragmentedFile(IFileSystem filesystem, string path, OpenMode mode) + { + IFile file; + + if (filesystem.FileExists($"{path}/00")) + { + filesystem.OpenFile(out file, $"{path}/00", mode); + } + else + { + filesystem.OpenFile(out file, path, mode); + } + + return file; + } + + private Stream GetZipStream(ZipArchiveEntry entry) + { + MemoryStream dest = new MemoryStream(); + + Stream src = entry.Open(); + + src.CopyTo(dest); + src.Dispose(); + + return dest; + } + + public SystemVersion VerifyFirmwarePackage(string firmwarePackage) + { + Dictionary> updateNcas = new Dictionary>(); + + if (Directory.Exists(firmwarePackage)) + { + return VerifyAndGetVersionDirectory(firmwarePackage); + } + + if (!File.Exists(firmwarePackage)) + { + throw new FileNotFoundException("Firmware file does not exist."); + } + + FileInfo info = new FileInfo(firmwarePackage); + + using (FileStream file = File.OpenRead(firmwarePackage)) + { + switch (info.Extension) + { + case ".zip": + using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage)) + { + return VerifyAndGetVersionZip(archive); + } + case ".xci": + Xci xci = new Xci(_device.System.KeySet, file.AsStorage()); + + if (xci.HasPartition(XciPartitionType.Update)) + { + XciPartition partition = xci.OpenPartition(XciPartitionType.Update); + + return VerifyAndGetVersion(partition); + } + else + { + throw new InvalidFirmwarePackageException("Update not found in xci file."); + } + default: + break; + } + } + + SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory) + { + return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory)); + } + + SystemVersion VerifyAndGetVersionZip(ZipArchive archive) + { + SystemVersion systemVersion = null; + + foreach (var entry in archive.Entries) + { + if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00")) + { + using (Stream ncaStream = GetZipStream(entry)) + { + IStorage storage = ncaStream.AsStorage(); + + Nca nca = new Nca(_device.System.KeySet, storage); + + if (updateNcas.ContainsKey(nca.Header.TitleId)) + { + updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName)); + } + else + { + updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>()); + updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName)); + } + } + } + } + + if (updateNcas.ContainsKey(SystemUpdateTitleId)) + { + var ncaEntry = updateNcas[SystemUpdateTitleId]; + + string metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path; + + CnmtContentMetaEntry[] metaEntries = null; + + var fileEntry = archive.GetEntry(metaPath); + + using (Stream ncaStream = GetZipStream(fileEntry)) + { + Nca metaNca = new Nca(_device.System.KeySet, ncaStream.AsStorage()); + + IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); + + string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath; + + if (fs.OpenFile(out IFile metaFile, cnmtPath, OpenMode.Read).IsSuccess()) + { + var meta = new Cnmt(metaFile.AsStream()); + + if (meta.Type == ContentMetaType.SystemUpdate) + { + metaEntries = meta.MetaEntries; + + updateNcas.Remove(SystemUpdateTitleId); + }; + } + } + + if (metaEntries == null) + { + throw new FileNotFoundException("System update title was not found in the firmware package."); + } + + if (updateNcas.ContainsKey(SystemVersionTitleId)) + { + string versionEntry = updateNcas[SystemVersionTitleId].Find(x => x.type != NcaContentType.Meta).path; + + using (Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry))) + { + Nca nca = new Nca(_device.System.KeySet, ncaStream.AsStorage()); + + var romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); + + if (romfs.OpenFile(out IFile systemVersionFile, "/file", OpenMode.Read).IsSuccess()) + { + systemVersion = new SystemVersion(systemVersionFile.AsStream()); + } + } + } + + foreach (CnmtContentMetaEntry metaEntry in metaEntries) + { + if (updateNcas.TryGetValue(metaEntry.TitleId, out ncaEntry)) + { + metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path; + + string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path; + + // Nintendo in 9.0.0, removed PPC and only kept the meta nca of it. + // This is a perfect valid case, so we should just ignore the missing content nca and continue. + if (contentPath == null) + { + updateNcas.Remove(metaEntry.TitleId); + + continue; + } + + ZipArchiveEntry metaZipEntry = archive.GetEntry(metaPath); + ZipArchiveEntry contentZipEntry = archive.GetEntry(contentPath); + + using (Stream metaNcaStream = GetZipStream(metaZipEntry)) + { + using (Stream contentNcaStream = GetZipStream(contentZipEntry)) + { + Nca metaNca = new Nca(_device.System.KeySet, metaNcaStream.AsStorage()); + + IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); + + string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath; + + if (fs.OpenFile(out IFile metaFile, cnmtPath, OpenMode.Read).IsSuccess()) + { + var meta = new Cnmt(metaFile.AsStream()); + + IStorage contentStorage = contentNcaStream.AsStorage(); + if (contentStorage.GetSize(out long size).IsSuccess()) + { + byte[] contentData = new byte[size]; + + Span content = new Span(contentData); + + contentStorage.Read(0, content); + + Span hash = new Span(new byte[32]); + + LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash); + + if (LibHac.Util.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash)) + { + updateNcas.Remove(metaEntry.TitleId); + } + } + } + } + } + } + } + + if (updateNcas.Count > 0) + { + string extraNcas = string.Empty; + + foreach (var entry in updateNcas) + { + foreach (var nca in entry.Value) + { + extraNcas += nca.path + Environment.NewLine; + } + } + + throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}"); + } + } + else + { + throw new FileNotFoundException("System update title was not found in the firmware package."); + } + + return systemVersion; + } + + SystemVersion VerifyAndGetVersion(IFileSystem filesystem) + { + SystemVersion systemVersion = null; + + CnmtContentMetaEntry[] metaEntries = null; + + foreach (var entry in filesystem.EnumerateEntries("/", "*.nca")) + { + IStorage ncaStorage = OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage(); + + Nca nca = new Nca(_device.System.KeySet, ncaStorage); + + if (nca.Header.TitleId == SystemUpdateTitleId && nca.Header.ContentType == NcaContentType.Meta) + { + IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); + + string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath; + + if (fs.OpenFile(out IFile metaFile, cnmtPath, OpenMode.Read).IsSuccess()) + { + var meta = new Cnmt(metaFile.AsStream()); + + if (meta.Type == ContentMetaType.SystemUpdate) + { + metaEntries = meta.MetaEntries; + } + }; + + continue; + } + else if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data) + { + var romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); + + if (romfs.OpenFile(out IFile systemVersionFile, "/file", OpenMode.Read).IsSuccess()) + { + systemVersion = new SystemVersion(systemVersionFile.AsStream()); + } + } + + if (updateNcas.ContainsKey(nca.Header.TitleId)) + { + updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath)); + } + else + { + updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>()); + updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath)); + } + + ncaStorage.Dispose(); + } + + if (metaEntries == null) + { + throw new FileNotFoundException("System update title was not found in the firmware package."); + } + + foreach (CnmtContentMetaEntry metaEntry in metaEntries) + { + if (updateNcas.TryGetValue(metaEntry.TitleId, out var ncaEntry)) + { + var metaNcaEntry = ncaEntry.Find(x => x.type == NcaContentType.Meta); + string contentPath = ncaEntry.Find(x => x.type != NcaContentType.Meta).path; + + // Nintendo in 9.0.0, removed PPC and only kept the meta nca of it. + // This is a perfect valid case, so we should just ignore the missing content nca and continue. + if (contentPath == null) + { + updateNcas.Remove(metaEntry.TitleId); + + continue; + } + + IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaEntry.path, OpenMode.Read).AsStorage(); + IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage(); + + Nca metaNca = new Nca(_device.System.KeySet, metaStorage); + + IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); + + string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath; + + if (fs.OpenFile(out IFile metaFile, cnmtPath, OpenMode.Read).IsSuccess()) + { + var meta = new Cnmt(metaFile.AsStream()); + + if (contentStorage.GetSize(out long size).IsSuccess()) + { + byte[] contentData = new byte[size]; + + Span content = new Span(contentData); + + contentStorage.Read(0, content); + + Span hash = new Span(new byte[32]); + + LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash); + + if (LibHac.Util.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash)) + { + updateNcas.Remove(metaEntry.TitleId); + } + } + } + } + } + + if (updateNcas.Count > 0) + { + string extraNcas = string.Empty; + + foreach (var entry in updateNcas) + { + foreach (var nca in entry.Value) + { + extraNcas += nca.path + Environment.NewLine; + } + } + + throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}"); + } + + return systemVersion; + } + + return null; + } + + public SystemVersion GetCurrentFirmwareVersion() + { + LoadEntries(true); + + var locationEnties = _locationEntries[StorageId.NandSystem]; + + foreach (var entry in locationEnties) + { + if (entry.ContentType == NcaContentType.Data) + { + var path = _device.FileSystem.SwitchPathToSystemPath(entry.ContentPath); + + using (IStorage ncaStorage = File.Open(path, FileMode.Open).AsStorage()) + { + Nca nca = new Nca(_device.System.KeySet, ncaStorage); + + if (nca.Header.TitleId == SystemVersionTitleId && nca.Header.ContentType == NcaContentType.Data) + { + var romfs = nca.OpenFileSystem(NcaSectionType.Data, _device.System.FsIntegrityCheckLevel); + + if (romfs.OpenFile(out IFile systemVersionFile, "/file", OpenMode.Read).IsSuccess()) + { + return new SystemVersion(systemVersionFile.AsStream()); + } + } + + } + } + } + + return null; + } } } diff --git a/Ryujinx.HLE/FileSystem/Content/SystemVersion.cs b/Ryujinx.HLE/FileSystem/Content/SystemVersion.cs new file mode 100644 index 0000000000..08ec351255 --- /dev/null +++ b/Ryujinx.HLE/FileSystem/Content/SystemVersion.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using System.Text; + +namespace Ryujinx.HLE.FileSystem.Content +{ + public class SystemVersion + { + public byte Major { get; } + public byte Minor { get; } + public byte Micro { get; } + public byte RevisionMajor { get; } + public byte RevisionMinor { get; } + public string PlatformString { get; } + public string Hex { get; } + public string VersionString { get; } + public string VersionTitle { get; } + + public SystemVersion(Stream systemVersionFile) + { + using (BinaryReader reader = new BinaryReader(systemVersionFile)) + { + Major = reader.ReadByte(); + Minor = reader.ReadByte(); + Micro = reader.ReadByte(); + + reader.ReadByte(); // Padding + + RevisionMajor = reader.ReadByte(); + RevisionMinor = reader.ReadByte(); + + reader.ReadBytes(2); // Padding + + PlatformString = Encoding.ASCII.GetString(reader.ReadBytes(0x20)).TrimEnd('\0'); + Hex = Encoding.ASCII.GetString(reader.ReadBytes(0x40)).TrimEnd('\0'); + VersionString = Encoding.ASCII.GetString(reader.ReadBytes(0x18)).TrimEnd('\0'); + VersionTitle = Encoding.ASCII.GetString(reader.ReadBytes(0x80)).TrimEnd('\0'); + } + } + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Font/SharedFontManager.cs b/Ryujinx.HLE/HOS/Font/SharedFontManager.cs index 99b662c0d1..e126cd5799 100644 --- a/Ryujinx.HLE/HOS/Font/SharedFontManager.cs +++ b/Ryujinx.HLE/HOS/Font/SharedFontManager.cs @@ -44,7 +44,15 @@ namespace Ryujinx.HLE.HOS.Font _fontsPath = Path.Combine(device.FileSystem.GetSystemPath(), "fonts"); } - public void EnsureInitialized(ContentManager contentManager) + public void Initialize(ContentManager contentManager, bool ignoreMissingFonts) + { + _fontData?.Clear(); + _fontData = null; + + EnsureInitialized(contentManager, ignoreMissingFonts); + } + + public void EnsureInitialized(ContentManager contentManager, bool ignoreMissingFonts) { if (_fontData == null) { @@ -112,10 +120,12 @@ namespace Ryujinx.HLE.HOS.Font return info; } - else + else if (!ignoreMissingFonts) { throw new InvalidSystemResourceException($"Font \"{name}.ttf\" not found. Please provide it in \"{_fontsPath}\"."); } + + return new FontInfo(); } _fontData = new Dictionary @@ -128,7 +138,7 @@ namespace Ryujinx.HLE.HOS.Font { SharedFontType.NintendoEx, CreateFont("FontNintendoExtended") } }; - if (fontOffset > Horizon.FontSize) + if (fontOffset > Horizon.FontSize && !ignoreMissingFonts) { throw new InvalidSystemResourceException( $"The sum of all fonts size exceed the shared memory size. " + @@ -151,14 +161,14 @@ namespace Ryujinx.HLE.HOS.Font public int GetFontSize(SharedFontType fontType) { - EnsureInitialized(_device.System.ContentManager); + EnsureInitialized(_device.System.ContentManager, false); return _fontData[fontType].Size; } public int GetSharedMemoryAddressOffset(SharedFontType fontType) { - EnsureInitialized(_device.System.ContentManager); + EnsureInitialized(_device.System.ContentManager, false); return _fontData[fontType].Offset + 8; } diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index 164a49a002..855d89148c 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -715,6 +715,21 @@ namespace Ryujinx.HLE.HOS } } + public SystemVersion VerifyFirmwarePackage(string firmwarePackage) + { + return ContentManager.VerifyFirmwarePackage(firmwarePackage); + } + + public SystemVersion GetCurrentFirmwareVersion() + { + return ContentManager.GetCurrentFirmwareVersion(); + } + + public void InstallFirmware(string firmwarePackage) + { + ContentManager.InstallFirmware(firmwarePackage); + } + public void SignalVsync() { VsyncEvent.ReadableEvent.Signal(); diff --git a/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs b/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs index 4560d9545c..418c15f2c2 100644 --- a/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs +++ b/Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs @@ -61,7 +61,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pl // GetSharedMemoryNativeHandle() -> handle public ResultCode GetSharedMemoryNativeHandle(ServiceCtx context) { - context.Device.System.Font.EnsureInitialized(context.Device.System.ContentManager); + context.Device.System.Font.EnsureInitialized(context.Device.System.ContentManager, false); if (context.Process.HandleTable.GenerateHandle(context.Device.System.FontSharedMem, out int handle) != KernelResult.Success) { diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index 2c4d011268..667ea5a5b9 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -44,6 +44,8 @@ namespace Ryujinx.Ui [GUI] CheckMenuItem _fullScreen; [GUI] MenuItem _stopEmulation; [GUI] CheckMenuItem _favToggle; + [GUI] MenuItem _firmwareInstallFile; + [GUI] MenuItem _firmwareInstallDirectory; [GUI] CheckMenuItem _iconToggle; [GUI] CheckMenuItem _appToggle; [GUI] CheckMenuItem _developerToggle; @@ -56,6 +58,7 @@ namespace Ryujinx.Ui [GUI] TreeView _gameTable; [GUI] TreeSelection _gameTableSelection; [GUI] Label _progressLabel; + [GUI] Label _firmwareVersionLabel; [GUI] LevelBar _progressBar; #pragma warning restore CS0649 #pragma warning restore IDE0044 @@ -134,6 +137,8 @@ namespace Ryujinx.Ui #pragma warning disable CS4014 UpdateGameTable(); #pragma warning restore CS4014 + + Task.Run(RefreshFirmwareLabel); } internal static void ApplyTheme() @@ -297,6 +302,9 @@ namespace Ryujinx.Ui _gameLoaded = true; _stopEmulation.Sensitive = true; + _firmwareInstallFile.Sensitive = false; + _firmwareInstallDirectory.Sensitive = false; + DiscordIntegrationModule.SwitchToPlayingState(_device.System.TitleId, _device.System.TitleName); string metadataFolder = System.IO.Path.Combine(new VirtualFileSystem().GetBasePath(), "games", _device.System.TitleId, "gui"); @@ -556,7 +564,199 @@ namespace Ryujinx.Ui _gameLoaded = false; } - private void FullScreen_Toggled(object sender, EventArgs args) + private void Installer_File_Pressed(object o, EventArgs args) + { + FileChooserDialog fileChooser = new FileChooserDialog("Choose the firmware file to open", + this, + FileChooserAction.Open, + "Cancel", + ResponseType.Cancel, + "Open", + ResponseType.Accept); + + fileChooser.Filter = new FileFilter(); + fileChooser.Filter.AddPattern("*.zip"); + fileChooser.Filter.AddPattern("*.xci"); + + HandleInstallerDialog(fileChooser); + } + + private void Installer_Directory_Pressed(object o, EventArgs args) + { + FileChooserDialog directoryChooser = new FileChooserDialog("Choose the firmware directory to open", + this, + FileChooserAction.SelectFolder, + "Cancel", + ResponseType.Cancel, + "Open", + ResponseType.Accept); + + HandleInstallerDialog(directoryChooser); + } + + private void RefreshFirmwareLabel() + { + var currentFirmware = _device.System.GetCurrentFirmwareVersion(); + + GLib.Idle.Add(new GLib.IdleHandler(() => + { + _firmwareVersionLabel.Text = currentFirmware != null ? currentFirmware.VersionString : "0.0.0"; + + return false; + })); + } + + private void HandleInstallerDialog(FileChooserDialog fileChooser) + { + if (fileChooser.Run() == (int)ResponseType.Accept) + { + MessageDialog dialog = null; + + try + { + string filename = fileChooser.Filename; + + fileChooser.Dispose(); + + var firmwareVersion = _device.System.VerifyFirmwarePackage(filename); + + if (firmwareVersion == null) + { + dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, ""); + + dialog.Text = "Firmware not found."; + + dialog.SecondaryText = $"A valid system firmware was not found in {filename}."; + + Logger.PrintError(LogClass.Application, $"A valid system firmware was not found in {filename}."); + + dialog.Run(); + dialog.Hide(); + dialog.Dispose(); + + return; + } + + var currentVersion = _device.System.GetCurrentFirmwareVersion(); + + string dialogMessage = $"System version {firmwareVersion.VersionString} will be installed."; + + if (currentVersion != null) + { + dialogMessage += $"This will replace the current system version {currentVersion.VersionString}. "; + } + + dialogMessage += "Do you want to continue?"; + + dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Question, ButtonsType.YesNo, false, ""); + + dialog.Text = $"Install Firmware {firmwareVersion.VersionString}"; + dialog.SecondaryText = dialogMessage; + + int response = dialog.Run(); + + dialog.Dispose(); + + dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.None, false, ""); + + dialog.Text = $"Install Firmware {firmwareVersion.VersionString}"; + + dialog.SecondaryText = "Installing firmware..."; + + if (response == (int)ResponseType.Yes) + { + Logger.PrintInfo(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}"); + + Thread thread = new Thread(() => + { + GLib.Idle.Add(new GLib.IdleHandler(() => + { + dialog.Run(); + return false; + })); + + try + { + _device.System.InstallFirmware(filename); + + GLib.Idle.Add(new GLib.IdleHandler(() => + { + dialog.Dispose(); + + dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, ""); + + dialog.Text = $"Install Firmware {firmwareVersion.VersionString}"; + + dialog.SecondaryText = $"System version {firmwareVersion.VersionString} successfully installed."; + + Logger.PrintInfo(LogClass.Application, $"System version {firmwareVersion.VersionString} successfully installed."); + + dialog.Run(); + dialog.Dispose(); + + return false; + })); + } + catch (Exception ex) + { + GLib.Idle.Add(new GLib.IdleHandler(() => + { + dialog.Dispose(); + + dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, ""); + + dialog.Text = $"Install Firmware {firmwareVersion.VersionString} Failed."; + + dialog.SecondaryText = $"An error occured while installing system version {firmwareVersion.VersionString}." + + " Please check logs for more info."; + + Logger.PrintError(LogClass.Application, ex.Message); + + dialog.Run(); + dialog.Dispose(); + + return false; + })); + } + finally + { + RefreshFirmwareLabel(); + } + }); + + thread.Start(); + } + else + { + dialog.Dispose(); + } + } + catch (Exception ex) + { + if (dialog != null) + { + dialog.Dispose(); + } + + dialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.Ok, false, ""); + + dialog.Text = "Parsing Firmware Failed."; + + dialog.SecondaryText = "An error occured while parsing firmware. Please check the logs for more info."; + + Logger.PrintError(LogClass.Application, ex.Message); + + dialog.Run(); + dialog.Dispose(); + } + } + else + { + fileChooser.Dispose(); + } + } + + private void FullScreen_Toggled(object o, EventArgs args) { if (_fullScreen.Active) { diff --git a/Ryujinx/Ui/MainWindow.glade b/Ryujinx/Ui/MainWindow.glade index fcf91bc4c5..8e2eab9391 100644 --- a/Ryujinx/Ui/MainWindow.glade +++ b/Ryujinx/Ui/MainWindow.glade @@ -1,5 +1,5 @@ - + @@ -8,9 +8,6 @@ center 1280 750 - - - True @@ -263,6 +260,44 @@ False Tools True + + + True + False + + + True + False + Install Firmware + True + + + True + False + + + True + False + Install a firmware from XCI or ZIP + True + + + + + + True + False + Install a firmware from a directory + True + + + + + + + + + @@ -370,7 +405,7 @@ True False - 5 + 10 5 2 2 @@ -388,7 +423,7 @@ True False start - 5 + 10 5 @@ -397,6 +432,57 @@ 2 + + + True + False + + + False + True + 3 + + + + + True + False + 5 + + + True + False + System Version + + + False + True + 0 + + + + + 50 + True + False + 5 + 5 + + + False + True + end + 1 + + + + + False + True + end + 4 + + False @@ -413,5 +499,8 @@ + + +