aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs9
-rw-r--r--Ryujinx.HLE/FileSystem/Content/ContentManager.cs573
-rw-r--r--Ryujinx.HLE/FileSystem/Content/SystemVersion.cs41
-rw-r--r--Ryujinx.HLE/HOS/Font/SharedFontManager.cs20
-rw-r--r--Ryujinx.HLE/HOS/Horizon.cs15
-rw-r--r--Ryujinx.HLE/HOS/Services/Sdb/Pl/ISharedFontManager.cs2
-rw-r--r--Ryujinx/Ui/MainWindow.cs202
-rw-r--r--Ryujinx/Ui/MainWindow.glade101
8 files changed, 936 insertions, 27 deletions
diff --git a/Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs b/Ryujinx.HLE/Exceptions/InvalidFirmwarePackageException.cs
new file mode 100644
index 00000000..bddd827a
--- /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 17faa19f..b83ae440 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<StorageId, LinkedList<LocationEntry>> _locationEntries;
- private Dictionary<string, long> _sharedFontTitleDictionary;
+ private Dictionary<string, long> _sharedFontTitleDictionary;
private Dictionary<string, string> _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<StorageId, LinkedList<LocationEntry>>();
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<LocationEntry> locationList = _locationEntries[storageId];
+ LinkedList<LocationEntry> locationList = _locationEntries[storageId];
LinkedListNode<LocationEntry> 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<ulong, List<(NcaContentType type, string path)>> updateNcas = new Dictionary<ulong, List<(NcaContentType, string)>>();
+
+ 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<byte> content = new Span<byte>(contentData);
+
+ contentStorage.Read(0, content);
+
+ Span<byte> hash = new Span<byte>(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<byte> content = new Span<byte>(contentData);
+
+ contentStorage.Read(0, content);
+
+ Span<byte> hash = new Span<byte>(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 00000000..08ec3512
--- /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 99b662c0..e126cd57 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<SharedFontType, FontInfo>
@@ -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 164a49a0..855d8914 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 4560d954..418c15f2 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<copy>
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 2c4d0112..667ea5a5 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 fcf91bc4..8e2eab93 100644
--- a/Ryujinx/Ui/MainWindow.glade
+++ b/Ryujinx/Ui/MainWindow.glade
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.22.1 -->
+<!-- Generated with glade 3.21.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkApplicationWindow" id="_mainWin">
@@ -9,9 +9,6 @@
<property name="default_width">1280</property>
<property name="default_height">750</property>
<child>
- <placeholder/>
- </child>
- <child>
<object class="GtkBox" id="_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -263,6 +260,44 @@
<property name="can_focus">False</property>
<property name="label" translatable="yes">Tools</property>
<property name="use_underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="FirmwareSubMenu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Install Firmware</property>
+ <property name="use_underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="_firmwareInstallFile">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Install a firmware from XCI or ZIP</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="Installer_File_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_firmwareInstallDirectory">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Install a firmware from a directory</property>
+ <property name="use_underline">True</property>
+ <signal name="activate" handler="Installer_Directory_Pressed" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
</object>
</child>
<child>
@@ -370,7 +405,7 @@
<object class="GtkLabel" id="_progressLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="margin_left">5</property>
+ <property name="margin_left">10</property>
<property name="margin_right">5</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
@@ -388,7 +423,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
- <property name="margin_left">5</property>
+ <property name="margin_left">10</property>
<property name="margin_right">5</property>
</object>
<packing>
@@ -397,6 +432,57 @@
<property name="position">2</property>
</packing>
</child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">System Version</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="_firmwareVersionLabel">
+ <property name="width_request">50</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">5</property>
+ <property name="margin_right">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
</object>
<packing>
<property name="expand">False</property>
@@ -413,5 +499,8 @@
</child>
</object>
</child>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
</object>
</interface>