forked from MeloNX/MeloNX
android - implement firmware installation
This commit is contained in:
parent
27059ded86
commit
2ef28525be
@ -250,6 +250,59 @@ namespace LibRyujinx
|
|||||||
return LoadApplication(stream, (FileType)(int)type);
|
return LoadApplication(stream, (FileType)(int)type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceVerifyFirmware")]
|
||||||
|
public static JLong JniVerifyFirmware(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
|
||||||
|
{
|
||||||
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
|
|
||||||
|
var stream = OpenFile(descriptor);
|
||||||
|
|
||||||
|
long stringHandle = -1;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var version = VerifyFirmware(stream, isXci);
|
||||||
|
|
||||||
|
if (version != null)
|
||||||
|
{
|
||||||
|
stringHandle = storeString(version.VersionString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception _)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceInstallFirmware")]
|
||||||
|
public static void JniInstallFirmware(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
|
||||||
|
{
|
||||||
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
|
|
||||||
|
var stream = OpenFile(descriptor);
|
||||||
|
|
||||||
|
InstallFirmware(stream, isXci);
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetInstalledFirmwareVersion")]
|
||||||
|
public static JLong JniGetInstalledFirmwareVersion(JEnvRef jEnv, JObjectLocalRef jObj)
|
||||||
|
{
|
||||||
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
|
|
||||||
|
var version = SwitchDevice?.ContentManager.GetCurrentFirmwareVersion();
|
||||||
|
|
||||||
|
long stringHandle = -1;
|
||||||
|
|
||||||
|
if (version != null)
|
||||||
|
{
|
||||||
|
stringHandle = storeString(version.VersionString);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringHandle;
|
||||||
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
|
||||||
public static JBoolean JniInitializeGraphicsNative(JEnvRef jEnv, JObjectLocalRef jObj, JObjectLocalRef graphicObject)
|
public static JBoolean JniInitializeGraphicsNative(JEnvRef jEnv, JObjectLocalRef jObj, JObjectLocalRef graphicObject)
|
||||||
{
|
{
|
||||||
|
@ -2,6 +2,7 @@ using ARMeilleure.Translation;
|
|||||||
using LibHac.Ncm;
|
using LibHac.Ncm;
|
||||||
using LibHac.Tools.FsSystem.NcaUtils;
|
using LibHac.Tools.FsSystem.NcaUtils;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
|
using Ryujinx.HLE.FileSystem;
|
||||||
using Ryujinx.HLE.HOS.SystemState;
|
using Ryujinx.HLE.HOS.SystemState;
|
||||||
using Ryujinx.Input.HLE;
|
using Ryujinx.Input.HLE;
|
||||||
using Silk.NET.Vulkan;
|
using Silk.NET.Vulkan;
|
||||||
@ -66,6 +67,16 @@ namespace LibRyujinx
|
|||||||
return LoadApplication(path);
|
return LoadApplication(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void InstallFirmware(Stream stream, bool isXci)
|
||||||
|
{
|
||||||
|
SwitchDevice?.ContentManager.InstallFirmware(stream, isXci);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SystemVersion? VerifyFirmware(Stream stream, bool isXci)
|
||||||
|
{
|
||||||
|
return SwitchDevice?.ContentManager?.VerifyFirmwarePackage(stream, isXci) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool LoadApplication(Stream stream, FileType type)
|
public static bool LoadApplication(Stream stream, FileType type)
|
||||||
{
|
{
|
||||||
var emulationContext = SwitchDevice.EmulationContext;
|
var emulationContext = SwitchDevice.EmulationContext;
|
||||||
|
@ -525,6 +525,27 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
FinishInstallation(temporaryDirectory, registeredDirectory);
|
FinishInstallation(temporaryDirectory, registeredDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void InstallFirmware(Stream stream, bool isXci)
|
||||||
|
{
|
||||||
|
string contentPathString = ContentPath.GetContentPath(StorageId.BuiltInSystem);
|
||||||
|
string contentDirectory = ContentPath.GetRealPath(contentPathString);
|
||||||
|
string registeredDirectory = Path.Combine(contentDirectory, "registered");
|
||||||
|
string temporaryDirectory = Path.Combine(contentDirectory, "temp");
|
||||||
|
|
||||||
|
if (!isXci)
|
||||||
|
{
|
||||||
|
using ZipArchive archive = new ZipArchive(stream);
|
||||||
|
InstallFromZip(archive, temporaryDirectory);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Xci xci = new(_virtualFileSystem.KeySet, stream.AsStorage());
|
||||||
|
InstallFromCart(xci, temporaryDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
FinishInstallation(temporaryDirectory, registeredDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
|
private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
|
||||||
{
|
{
|
||||||
if (Directory.Exists(registeredDirectory))
|
if (Directory.Exists(registeredDirectory))
|
||||||
@ -643,13 +664,16 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers.");
|
throw new MissingKeyException("HeaderKey is empty. Cannot decrypt NCA headers.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
|
|
||||||
|
|
||||||
if (Directory.Exists(firmwarePackage))
|
if (Directory.Exists(firmwarePackage))
|
||||||
{
|
{
|
||||||
return VerifyAndGetVersionDirectory(firmwarePackage);
|
return VerifyAndGetVersionDirectory(firmwarePackage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
|
||||||
|
{
|
||||||
|
return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
if (!File.Exists(firmwarePackage))
|
if (!File.Exists(firmwarePackage))
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException("Firmware file does not exist.");
|
throw new FileNotFoundException("Firmware file does not exist.");
|
||||||
@ -657,249 +681,99 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
|
|
||||||
FileInfo info = new(firmwarePackage);
|
FileInfo info = new(firmwarePackage);
|
||||||
|
|
||||||
using FileStream file = File.OpenRead(firmwarePackage);
|
|
||||||
|
|
||||||
switch (info.Extension)
|
if (info.Extension == ".zip" || info.Extension == ".xci")
|
||||||
{
|
{
|
||||||
case ".zip":
|
using FileStream file = File.OpenRead(firmwarePackage);
|
||||||
using (ZipArchive archive = ZipFile.OpenRead(firmwarePackage))
|
|
||||||
{
|
|
||||||
return VerifyAndGetVersionZip(archive);
|
|
||||||
}
|
|
||||||
case ".xci":
|
|
||||||
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
|
|
||||||
|
|
||||||
if (xci.HasPartition(XciPartitionType.Update))
|
var isXci = info.Extension == ".xci";
|
||||||
{
|
|
||||||
XciPartition partition = xci.OpenPartition(XciPartitionType.Update);
|
|
||||||
|
|
||||||
return VerifyAndGetVersion(partition);
|
return VerifyFirmwarePackage(file, isXci);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidFirmwarePackageException("Update not found in xci file.");
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemVersion VerifyAndGetVersionDirectory(string firmwareDirectory)
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SystemVersion VerifyFirmwarePackage(Stream file, bool isXci)
|
||||||
|
{
|
||||||
|
if (!isXci)
|
||||||
{
|
{
|
||||||
return VerifyAndGetVersion(new LocalFileSystem(firmwareDirectory));
|
using ZipArchive archive = new ZipArchive(file, ZipArchiveMode.Read);
|
||||||
|
return VerifyAndGetVersionZip(archive);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
|
|
||||||
{
|
{
|
||||||
SystemVersion systemVersion = null;
|
Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
|
||||||
|
|
||||||
foreach (var entry in archive.Entries)
|
if (xci.HasPartition(XciPartitionType.Update))
|
||||||
{
|
{
|
||||||
if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
|
XciPartition partition = xci.OpenPartition(XciPartitionType.Update);
|
||||||
{
|
|
||||||
using Stream ncaStream = GetZipStream(entry);
|
|
||||||
IStorage storage = ncaStream.AsStorage();
|
|
||||||
|
|
||||||
Nca nca = new(_virtualFileSystem.KeySet, storage);
|
return VerifyAndGetVersion(partition);
|
||||||
|
|
||||||
if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
|
|
||||||
{
|
|
||||||
updateNcasItem.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.TryGetValue(SystemUpdateTitleId, out var ncaEntry))
|
|
||||||
{
|
|
||||||
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(_virtualFileSystem.KeySet, ncaStream.AsStorage());
|
|
||||||
|
|
||||||
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
|
||||||
|
|
||||||
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
|
|
||||||
|
|
||||||
using var metaFile = new UniqueRef<IFile>();
|
|
||||||
|
|
||||||
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
|
|
||||||
{
|
|
||||||
var meta = new Cnmt(metaFile.Get.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.TryGetValue(SystemVersionTitleId, out var updateNcasItem))
|
|
||||||
{
|
|
||||||
string versionEntry = updateNcasItem.Find(x => x.type != NcaContentType.Meta).path;
|
|
||||||
|
|
||||||
using Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry));
|
|
||||||
Nca nca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
|
|
||||||
|
|
||||||
var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
|
||||||
|
|
||||||
using var systemVersionFile = new UniqueRef<IFile>();
|
|
||||||
|
|
||||||
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
|
|
||||||
{
|
|
||||||
systemVersion = new SystemVersion(systemVersionFile.Get.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(_virtualFileSystem.KeySet, metaNcaStream.AsStorage());
|
|
||||||
|
|
||||||
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
|
||||||
|
|
||||||
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
|
|
||||||
|
|
||||||
using var metaFile = new UniqueRef<IFile>();
|
|
||||||
|
|
||||||
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
|
|
||||||
{
|
|
||||||
var meta = new Cnmt(metaFile.Get.AsStream());
|
|
||||||
|
|
||||||
IStorage contentStorage = contentNcaStream.AsStorage();
|
|
||||||
if (contentStorage.GetSize(out long size).IsSuccess())
|
|
||||||
{
|
|
||||||
byte[] contentData = new byte[size];
|
|
||||||
|
|
||||||
Span<byte> content = new(contentData);
|
|
||||||
|
|
||||||
contentStorage.Read(0, content);
|
|
||||||
|
|
||||||
Span<byte> hash = new(new byte[32]);
|
|
||||||
|
|
||||||
LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash);
|
|
||||||
|
|
||||||
if (LibHac.Common.Utilities.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash))
|
|
||||||
{
|
|
||||||
updateNcas.Remove(metaEntry.TitleId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateNcas.Count > 0)
|
|
||||||
{
|
|
||||||
StringBuilder extraNcas = new();
|
|
||||||
|
|
||||||
foreach (var entry in updateNcas)
|
|
||||||
{
|
|
||||||
foreach (var (type, path) in entry.Value)
|
|
||||||
{
|
|
||||||
extraNcas.AppendLine(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException("System update title was not found in the firmware package.");
|
throw new InvalidFirmwarePackageException("Update not found in xci file.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return systemVersion;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
|
private SystemVersion VerifyAndGetVersionZip(ZipArchive archive)
|
||||||
|
{
|
||||||
|
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
|
||||||
|
|
||||||
|
SystemVersion systemVersion = null;
|
||||||
|
|
||||||
|
foreach (var entry in archive.Entries)
|
||||||
{
|
{
|
||||||
SystemVersion systemVersion = null;
|
if (entry.FullName.EndsWith(".nca") || entry.FullName.EndsWith(".nca/00"))
|
||||||
|
|
||||||
CnmtContentMetaEntry[] metaEntries = null;
|
|
||||||
|
|
||||||
foreach (var entry in filesystem.EnumerateEntries("/", "*.nca"))
|
|
||||||
{
|
{
|
||||||
IStorage ncaStorage = OpenPossibleFragmentedFile(filesystem, entry.FullPath, OpenMode.Read).AsStorage();
|
using Stream ncaStream = GetZipStream(entry);
|
||||||
|
IStorage storage = ncaStream.AsStorage();
|
||||||
|
|
||||||
Nca nca = new(_virtualFileSystem.KeySet, ncaStorage);
|
Nca nca = new(_virtualFileSystem.KeySet, storage);
|
||||||
|
|
||||||
if (nca.Header.TitleId == SystemUpdateTitleId && nca.Header.ContentType == NcaContentType.Meta)
|
|
||||||
{
|
|
||||||
IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
|
||||||
|
|
||||||
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
|
|
||||||
|
|
||||||
using var metaFile = new UniqueRef<IFile>();
|
|
||||||
|
|
||||||
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
|
|
||||||
{
|
|
||||||
var meta = new Cnmt(metaFile.Get.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, IntegrityCheckLevel.ErrorOnInvalid);
|
|
||||||
|
|
||||||
using var systemVersionFile = new UniqueRef<IFile>();
|
|
||||||
|
|
||||||
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
|
|
||||||
{
|
|
||||||
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
|
if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
|
||||||
{
|
{
|
||||||
updateNcasItem.Add((nca.Header.ContentType, entry.FullPath));
|
updateNcasItem.Add((nca.Header.ContentType, entry.FullName));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
|
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
|
||||||
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath));
|
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ncaStorage.Dispose();
|
if (updateNcas.TryGetValue(SystemUpdateTitleId, out var ncaEntry))
|
||||||
|
{
|
||||||
|
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(_virtualFileSystem.KeySet, ncaStream.AsStorage());
|
||||||
|
|
||||||
|
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
|
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
|
||||||
|
|
||||||
|
using var metaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
|
||||||
|
{
|
||||||
|
var meta = new Cnmt(metaFile.Get.AsStream());
|
||||||
|
|
||||||
|
if (meta.Type == ContentMetaType.SystemUpdate)
|
||||||
|
{
|
||||||
|
metaEntries = meta.MetaEntries;
|
||||||
|
|
||||||
|
updateNcas.Remove(SystemUpdateTitleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metaEntries == null)
|
if (metaEntries == null)
|
||||||
@ -907,11 +781,29 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
throw new FileNotFoundException("System update title was not found in the firmware package.");
|
throw new FileNotFoundException("System update title was not found in the firmware package.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updateNcas.TryGetValue(SystemVersionTitleId, out var updateNcasItem))
|
||||||
|
{
|
||||||
|
string versionEntry = updateNcasItem.Find(x => x.type != NcaContentType.Meta).path;
|
||||||
|
|
||||||
|
using Stream ncaStream = GetZipStream(archive.GetEntry(versionEntry));
|
||||||
|
Nca nca = new(_virtualFileSystem.KeySet, ncaStream.AsStorage());
|
||||||
|
|
||||||
|
var romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
|
using var systemVersionFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
|
||||||
|
{
|
||||||
|
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (CnmtContentMetaEntry metaEntry in metaEntries)
|
foreach (CnmtContentMetaEntry metaEntry in metaEntries)
|
||||||
{
|
{
|
||||||
if (updateNcas.TryGetValue(metaEntry.TitleId, out var ncaEntry))
|
if (updateNcas.TryGetValue(metaEntry.TitleId, out ncaEntry))
|
||||||
{
|
{
|
||||||
string metaNcaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
|
metaPath = ncaEntry.Find(x => x.type == NcaContentType.Meta).path;
|
||||||
|
|
||||||
string contentPath = 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.
|
// Nintendo in 9.0.0, removed PPC and only kept the meta nca of it.
|
||||||
@ -923,10 +815,12 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaPath, OpenMode.Read).AsStorage();
|
ZipArchiveEntry metaZipEntry = archive.GetEntry(metaPath);
|
||||||
IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage();
|
ZipArchiveEntry contentZipEntry = archive.GetEntry(contentPath);
|
||||||
|
|
||||||
Nca metaNca = new(_virtualFileSystem.KeySet, metaStorage);
|
using Stream metaNcaStream = GetZipStream(metaZipEntry);
|
||||||
|
using Stream contentNcaStream = GetZipStream(contentZipEntry);
|
||||||
|
Nca metaNca = new(_virtualFileSystem.KeySet, metaNcaStream.AsStorage());
|
||||||
|
|
||||||
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
@ -938,6 +832,7 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
{
|
{
|
||||||
var meta = new Cnmt(metaFile.Get.AsStream());
|
var meta = new Cnmt(metaFile.Get.AsStream());
|
||||||
|
|
||||||
|
IStorage contentStorage = contentNcaStream.AsStorage();
|
||||||
if (contentStorage.GetSize(out long size).IsSuccess())
|
if (contentStorage.GetSize(out long size).IsSuccess())
|
||||||
{
|
{
|
||||||
byte[] contentData = new byte[size];
|
byte[] contentData = new byte[size];
|
||||||
@ -973,11 +868,147 @@ namespace Ryujinx.HLE.FileSystem
|
|||||||
|
|
||||||
throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
|
throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return systemVersion;
|
else
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("System update title was not found in the firmware package.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return systemVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SystemVersion VerifyAndGetVersion(IFileSystem filesystem)
|
||||||
|
{
|
||||||
|
Dictionary<ulong, List<(NcaContentType type, string path)>> updateNcas = new();
|
||||||
|
|
||||||
|
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(_virtualFileSystem.KeySet, ncaStorage);
|
||||||
|
|
||||||
|
if (nca.Header.TitleId == SystemUpdateTitleId && nca.Header.ContentType == NcaContentType.Meta)
|
||||||
|
{
|
||||||
|
IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
|
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
|
||||||
|
|
||||||
|
using var metaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
|
||||||
|
{
|
||||||
|
var meta = new Cnmt(metaFile.Get.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, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
|
using var systemVersionFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
if (romfs.OpenFile(ref systemVersionFile.Ref, "/file".ToU8Span(), OpenMode.Read).IsSuccess())
|
||||||
|
{
|
||||||
|
systemVersion = new SystemVersion(systemVersionFile.Get.AsStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateNcas.TryGetValue(nca.Header.TitleId, out var updateNcasItem))
|
||||||
|
{
|
||||||
|
updateNcasItem.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))
|
||||||
|
{
|
||||||
|
string metaNcaPath = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
IStorage metaStorage = OpenPossibleFragmentedFile(filesystem, metaNcaPath, OpenMode.Read).AsStorage();
|
||||||
|
IStorage contentStorage = OpenPossibleFragmentedFile(filesystem, contentPath, OpenMode.Read).AsStorage();
|
||||||
|
|
||||||
|
Nca metaNca = new(_virtualFileSystem.KeySet, metaStorage);
|
||||||
|
|
||||||
|
IFileSystem fs = metaNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
|
||||||
|
|
||||||
|
string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath;
|
||||||
|
|
||||||
|
using var metaFile = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
if (fs.OpenFile(ref metaFile.Ref, cnmtPath.ToU8Span(), OpenMode.Read).IsSuccess())
|
||||||
|
{
|
||||||
|
var meta = new Cnmt(metaFile.Get.AsStream());
|
||||||
|
|
||||||
|
if (contentStorage.GetSize(out long size).IsSuccess())
|
||||||
|
{
|
||||||
|
byte[] contentData = new byte[size];
|
||||||
|
|
||||||
|
Span<byte> content = new(contentData);
|
||||||
|
|
||||||
|
contentStorage.Read(0, content);
|
||||||
|
|
||||||
|
Span<byte> hash = new(new byte[32]);
|
||||||
|
|
||||||
|
LibHac.Crypto.Sha256.GenerateSha256Hash(content, hash);
|
||||||
|
|
||||||
|
if (LibHac.Common.Utilities.ArraysEqual(hash.ToArray(), meta.ContentEntries[0].Hash))
|
||||||
|
{
|
||||||
|
updateNcas.Remove(metaEntry.TitleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateNcas.Count > 0)
|
||||||
|
{
|
||||||
|
StringBuilder extraNcas = new();
|
||||||
|
|
||||||
|
foreach (var entry in updateNcas)
|
||||||
|
{
|
||||||
|
foreach (var (type, path) in entry.Value)
|
||||||
|
{
|
||||||
|
extraNcas.AppendLine(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidFirmwarePackageException($"Firmware package contains unrelated archives. Please remove these paths: {Environment.NewLine}{extraNcas}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SystemVersion GetCurrentFirmwareVersion()
|
public SystemVersion GetCurrentFirmwareVersion()
|
||||||
|
@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "org.ryujinx.android"
|
applicationId "org.ryujinx.android"
|
||||||
minSdk 30
|
minSdk 30
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 10008
|
versionCode 10010
|
||||||
versionName '1.0.8'
|
versionName '1.0.10'
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@ -49,6 +49,7 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose true
|
compose true
|
||||||
prefab true
|
prefab true
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion '1.3.2'
|
kotlinCompilerExtensionVersion '1.3.2'
|
||||||
|
@ -102,6 +102,8 @@ class MainActivity : BaseActivity() {
|
|||||||
mainViewModel!!.physicalControllerManager = physicalControllerManager
|
mainViewModel!!.physicalControllerManager = physicalControllerManager
|
||||||
mainViewModel!!.motionSensorManager = motionSensorManager
|
mainViewModel!!.motionSensorManager = motionSensorManager
|
||||||
|
|
||||||
|
mainViewModel!!.refreshFirmwareVersion()
|
||||||
|
|
||||||
mainViewModel?.apply {
|
mainViewModel?.apply {
|
||||||
setContent {
|
setContent {
|
||||||
RyujinxAndroidTheme {
|
RyujinxAndroidTheme {
|
||||||
|
@ -8,20 +8,24 @@ class RyujinxNative {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val instance: RyujinxNative = RyujinxNative()
|
val instance: RyujinxNative = RyujinxNative()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
System.loadLibrary("ryujinx")
|
System.loadLibrary("ryujinx")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
external fun deviceInitialize(isHostMapped: Boolean, useNce: Boolean,
|
external fun deviceInitialize(
|
||||||
systemLanguage : Int,
|
isHostMapped: Boolean, useNce: Boolean,
|
||||||
regionCode : Int,
|
systemLanguage: Int,
|
||||||
enableVsync : Boolean,
|
regionCode: Int,
|
||||||
enableDockedMode : Boolean,
|
enableVsync: Boolean,
|
||||||
enablePtc : Boolean,
|
enableDockedMode: Boolean,
|
||||||
enableInternetAccess : Boolean,
|
enablePtc: Boolean,
|
||||||
timeZone : Long,
|
enableInternetAccess: Boolean,
|
||||||
ignoreMissingServices : Boolean): Boolean
|
timeZone: Long,
|
||||||
|
ignoreMissingServices: Boolean
|
||||||
|
): Boolean
|
||||||
|
|
||||||
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
|
external fun graphicsInitialize(configuration: GraphicsConfiguration): Boolean
|
||||||
external fun graphicsInitializeRenderer(
|
external fun graphicsInitializeRenderer(
|
||||||
extensions: Array<String>,
|
extensions: Array<String>,
|
||||||
@ -35,7 +39,7 @@ class RyujinxNative {
|
|||||||
external fun deviceGetGameFifo(): Double
|
external fun deviceGetGameFifo(): Double
|
||||||
external fun deviceGetGameInfo(fileDescriptor: Int, extension: Long): GameInfo
|
external fun deviceGetGameInfo(fileDescriptor: Int, extension: Long): GameInfo
|
||||||
external fun deviceGetGameInfoFromPath(path: String): GameInfo
|
external fun deviceGetGameInfoFromPath(path: String): GameInfo
|
||||||
external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int): Boolean
|
external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int): Boolean
|
||||||
external fun graphicsRendererSetSize(width: Int, height: Int)
|
external fun graphicsRendererSetSize(width: Int, height: Int)
|
||||||
external fun graphicsRendererSetVsync(enabled: Boolean)
|
external fun graphicsRendererSetVsync(enabled: Boolean)
|
||||||
external fun graphicsRendererRunLoop()
|
external fun graphicsRendererRunLoop()
|
||||||
@ -54,17 +58,20 @@ class RyujinxNative {
|
|||||||
external fun graphicsSetSurface(surface: Long, window: Long)
|
external fun graphicsSetSurface(surface: Long, window: Long)
|
||||||
external fun deviceCloseEmulation()
|
external fun deviceCloseEmulation()
|
||||||
external fun deviceSignalEmulationClose()
|
external fun deviceSignalEmulationClose()
|
||||||
external fun deviceGetDlcTitleId(path: Long, ncaPath: Long) : Long
|
external fun deviceGetDlcTitleId(path: Long, ncaPath: Long): Long
|
||||||
external fun deviceGetDlcContentList(path: Long, titleId: Long) : Array<String>
|
external fun deviceGetDlcContentList(path: Long, titleId: Long): Array<String>
|
||||||
external fun userGetOpenedUser() : Long
|
external fun userGetOpenedUser(): Long
|
||||||
external fun userGetUserPicture(userId: Long) : Long
|
external fun userGetUserPicture(userId: Long): Long
|
||||||
external fun userSetUserPicture(userId: String, picture: String)
|
external fun userSetUserPicture(userId: String, picture: String)
|
||||||
external fun userGetUserName(userId: Long) : Long
|
external fun userGetUserName(userId: Long): Long
|
||||||
external fun userSetUserName(userId: String, userName: String)
|
external fun userSetUserName(userId: String, userName: String)
|
||||||
external fun userGetAllUsers() : Array<String>
|
external fun userGetAllUsers(): Array<String>
|
||||||
external fun userAddUser(username: String, picture: String)
|
external fun userAddUser(username: String, picture: String)
|
||||||
external fun userDeleteUser(userId: String)
|
external fun userDeleteUser(userId: String)
|
||||||
external fun userOpenUser(userId: Long)
|
external fun userOpenUser(userId: Long)
|
||||||
external fun userCloseUser(userId: String)
|
external fun userCloseUser(userId: String)
|
||||||
external fun loggingSetEnabled(logLevel: Int, enabled: Boolean)
|
external fun loggingSetEnabled(logLevel: Int, enabled: Boolean)
|
||||||
|
external fun deviceVerifyFirmware(fileDescriptor: Int, isXci: Boolean): Long
|
||||||
|
external fun deviceInstallFirmware(fileDescriptor: Int, isXci: Boolean)
|
||||||
|
external fun deviceGetInstalledFirmwareVersion() : Long
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
var isMiiEditorLaunched = false
|
var isMiiEditorLaunched = false
|
||||||
val userViewModel = UserViewModel()
|
val userViewModel = UserViewModel()
|
||||||
val logging = Logging(this)
|
val logging = Logging(this)
|
||||||
|
var firmwareVersion = ""
|
||||||
private var gameTimeState: MutableState<Double>? = null
|
private var gameTimeState: MutableState<Double>? = null
|
||||||
private var gameFpsState: MutableState<Double>? = null
|
private var gameFpsState: MutableState<Double>? = null
|
||||||
private var fifoState: MutableState<Double>? = null
|
private var fifoState: MutableState<Double>? = null
|
||||||
@ -69,6 +70,13 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
motionSensorManager?.setControllerId(-1)
|
motionSensorManager?.setControllerId(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshFirmwareVersion(){
|
||||||
|
var handle = RyujinxNative.instance.deviceGetInstalledFirmwareVersion()
|
||||||
|
if(handle != -1L) {
|
||||||
|
firmwareVersion = NativeHelpers.instance.getStringJava(handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun loadGame(game:GameModel) : Boolean {
|
fun loadGame(game:GameModel) : Boolean {
|
||||||
val nativeRyujinx = RyujinxNative.instance
|
val nativeRyujinx = RyujinxNative.instance
|
||||||
|
|
||||||
@ -178,8 +186,6 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun loadMiiEditor() : Boolean {
|
fun loadMiiEditor() : Boolean {
|
||||||
val nativeRyujinx = RyujinxNative.instance
|
val nativeRyujinx = RyujinxNative.instance
|
||||||
|
|
||||||
|
@ -5,30 +5,40 @@ import androidx.compose.runtime.MutableState
|
|||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.anggrayudi.storage.callback.FileCallback
|
||||||
import com.anggrayudi.storage.file.FileFullPath
|
import com.anggrayudi.storage.file.FileFullPath
|
||||||
|
import com.anggrayudi.storage.file.copyFileTo
|
||||||
|
import com.anggrayudi.storage.file.extension
|
||||||
import com.anggrayudi.storage.file.getAbsolutePath
|
import com.anggrayudi.storage.file.getAbsolutePath
|
||||||
import org.ryujinx.android.LogLevel
|
import org.ryujinx.android.LogLevel
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
|
import org.ryujinx.android.NativeHelpers
|
||||||
import org.ryujinx.android.RyujinxNative
|
import org.ryujinx.android.RyujinxNative
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
class SettingsViewModel(var navController: NavHostController, val activity: MainActivity) {
|
class SettingsViewModel(var navController: NavHostController, val activity: MainActivity) {
|
||||||
private var previousCallback: ((requestCode: Int, folder: DocumentFile) -> Unit)?
|
var selectedFirmwareVersion: String = ""
|
||||||
|
private var previousFileCallback: ((requestCode: Int, files: List<DocumentFile>) -> Unit)?
|
||||||
|
private var previousFolderCallback: ((requestCode: Int, folder: DocumentFile) -> Unit)?
|
||||||
private var sharedPref: SharedPreferences
|
private var sharedPref: SharedPreferences
|
||||||
|
var selectedFirmwareFile: DocumentFile? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
sharedPref = getPreferences()
|
sharedPref = getPreferences()
|
||||||
previousCallback = activity.storageHelper!!.onFolderSelected
|
previousFolderCallback = activity.storageHelper!!.onFolderSelected
|
||||||
|
previousFileCallback = activity.storageHelper!!.onFileSelected
|
||||||
activity.storageHelper!!.onFolderSelected = { requestCode, folder ->
|
activity.storageHelper!!.onFolderSelected = { requestCode, folder ->
|
||||||
run {
|
run {
|
||||||
val p = folder.getAbsolutePath(activity!!)
|
val p = folder.getAbsolutePath(activity)
|
||||||
val editor = sharedPref?.edit()
|
val editor = sharedPref.edit()
|
||||||
editor?.putString("gameFolder", p)
|
editor?.putString("gameFolder", p)
|
||||||
editor?.apply()
|
editor?.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPreferences() : SharedPreferences {
|
private fun getPreferences(): SharedPreferences {
|
||||||
return PreferenceManager.getDefaultSharedPreferences(activity)
|
return PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,8 +62,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
|||||||
enableGuestLogs: MutableState<Boolean>,
|
enableGuestLogs: MutableState<Boolean>,
|
||||||
enableAccessLogs: MutableState<Boolean>,
|
enableAccessLogs: MutableState<Boolean>,
|
||||||
enableTraceLogs: MutableState<Boolean>
|
enableTraceLogs: MutableState<Boolean>
|
||||||
)
|
) {
|
||||||
{
|
|
||||||
|
|
||||||
isHostMapped.value = sharedPref.getBoolean("isHostMapped", true)
|
isHostMapped.value = sharedPref.getBoolean("isHostMapped", true)
|
||||||
useNce.value = sharedPref.getBoolean("useNce", true)
|
useNce.value = sharedPref.getBoolean("useNce", true)
|
||||||
@ -62,7 +71,8 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
|||||||
enablePtc.value = sharedPref.getBoolean("enablePtc", true)
|
enablePtc.value = sharedPref.getBoolean("enablePtc", true)
|
||||||
ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false)
|
ignoreMissingServices.value = sharedPref.getBoolean("ignoreMissingServices", false)
|
||||||
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
|
enableShaderCache.value = sharedPref.getBoolean("enableShaderCache", true)
|
||||||
enableTextureRecompression.value = sharedPref.getBoolean("enableTextureRecompression", false)
|
enableTextureRecompression.value =
|
||||||
|
sharedPref.getBoolean("enableTextureRecompression", false)
|
||||||
resScale.value = sharedPref.getFloat("resScale", 1f)
|
resScale.value = sharedPref.getFloat("resScale", 1f)
|
||||||
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
|
useVirtualController.value = sharedPref.getBoolean("useVirtualController", true)
|
||||||
isGrid.value = sharedPref.getBoolean("isGrid", true)
|
isGrid.value = sharedPref.getBoolean("isGrid", true)
|
||||||
@ -97,7 +107,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
|||||||
enableGuestLogs: MutableState<Boolean>,
|
enableGuestLogs: MutableState<Boolean>,
|
||||||
enableAccessLogs: MutableState<Boolean>,
|
enableAccessLogs: MutableState<Boolean>,
|
||||||
enableTraceLogs: MutableState<Boolean>
|
enableTraceLogs: MutableState<Boolean>
|
||||||
){
|
) {
|
||||||
val editor = sharedPref.edit()
|
val editor = sharedPref.edit()
|
||||||
|
|
||||||
editor.putBoolean("isHostMapped", isHostMapped.value)
|
editor.putBoolean("isHostMapped", isHostMapped.value)
|
||||||
@ -123,7 +133,7 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
|||||||
editor.putBoolean("enableTraceLogs", enableTraceLogs.value)
|
editor.putBoolean("enableTraceLogs", enableTraceLogs.value)
|
||||||
|
|
||||||
editor.apply()
|
editor.apply()
|
||||||
activity.storageHelper!!.onFolderSelected = previousCallback
|
activity.storageHelper!!.onFolderSelected = previousFolderCallback
|
||||||
|
|
||||||
RyujinxNative.instance.loggingSetEnabled(LogLevel.Debug.ordinal, enableDebugLogs.value)
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Debug.ordinal, enableDebugLogs.value)
|
||||||
RyujinxNative.instance.loggingSetEnabled(LogLevel.Info.ordinal, enableInfoLogs.value)
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Info.ordinal, enableInfoLogs.value)
|
||||||
@ -135,17 +145,122 @@ class SettingsViewModel(var navController: NavHostController, val activity: Main
|
|||||||
RyujinxNative.instance.loggingSetEnabled(LogLevel.Trace.ordinal, enableTraceLogs.value)
|
RyujinxNative.instance.loggingSetEnabled(LogLevel.Trace.ordinal, enableTraceLogs.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun openGameFolder() {
|
fun openGameFolder() {
|
||||||
val path = sharedPref?.getString("gameFolder", "") ?: ""
|
val path = sharedPref?.getString("gameFolder", "") ?: ""
|
||||||
|
|
||||||
if (path.isEmpty())
|
if (path.isEmpty())
|
||||||
activity?.storageHelper?.storage?.openFolderPicker()
|
activity.storageHelper?.storage?.openFolderPicker()
|
||||||
else
|
else
|
||||||
activity?.storageHelper?.storage?.openFolderPicker(
|
activity.storageHelper?.storage?.openFolderPicker(
|
||||||
activity.storageHelper!!.storage.requestCodeFolderPicker,
|
activity.storageHelper!!.storage.requestCodeFolderPicker,
|
||||||
FileFullPath(activity, path)
|
FileFullPath(activity, path)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun importProdKeys() {
|
||||||
|
activity.storageHelper!!.onFileSelected = { requestCode, files ->
|
||||||
|
run {
|
||||||
|
activity.storageHelper!!.onFileSelected = previousFileCallback
|
||||||
|
val file = files.firstOrNull()
|
||||||
|
file?.apply {
|
||||||
|
if (name == "prod.keys") {
|
||||||
|
val outputFile = File(MainActivity.AppPath + "/system");
|
||||||
|
outputFile.delete()
|
||||||
|
|
||||||
|
thread {
|
||||||
|
file.copyFileTo(
|
||||||
|
activity,
|
||||||
|
outputFile,
|
||||||
|
callback = object : FileCallback() {
|
||||||
|
override fun onCompleted(result: Any) {
|
||||||
|
super.onCompleted(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activity.storageHelper?.storage?.openFilePicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectFirmware(installState: MutableState<FirmwareInstallState>) {
|
||||||
|
if (installState.value != FirmwareInstallState.None)
|
||||||
|
return
|
||||||
|
activity.storageHelper!!.onFileSelected = { _, files ->
|
||||||
|
run {
|
||||||
|
activity.storageHelper!!.onFileSelected = previousFileCallback
|
||||||
|
val file = files.firstOrNull()
|
||||||
|
file?.apply {
|
||||||
|
if (extension == "xci" || extension == "zip") {
|
||||||
|
installState.value = FirmwareInstallState.Verifying
|
||||||
|
thread {
|
||||||
|
val descriptor =
|
||||||
|
activity.contentResolver.openFileDescriptor(file.uri, "rw")
|
||||||
|
descriptor?.use { d ->
|
||||||
|
val version = RyujinxNative.instance.deviceVerifyFirmware(
|
||||||
|
d.fd,
|
||||||
|
extension == "xci"
|
||||||
|
)
|
||||||
|
selectedFirmwareFile = file
|
||||||
|
if (version != -1L) {
|
||||||
|
selectedFirmwareVersion =
|
||||||
|
NativeHelpers.instance.getStringJava(version)
|
||||||
|
installState.value = FirmwareInstallState.Query
|
||||||
|
} else {
|
||||||
|
installState.value = FirmwareInstallState.Cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
installState.value = FirmwareInstallState.Cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activity.storageHelper?.storage?.openFilePicker()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installFirmware(installState: MutableState<FirmwareInstallState>) {
|
||||||
|
if (installState.value != FirmwareInstallState.Query)
|
||||||
|
return
|
||||||
|
if (selectedFirmwareFile == null) {
|
||||||
|
installState.value = FirmwareInstallState.None
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedFirmwareFile?.apply {
|
||||||
|
val descriptor =
|
||||||
|
activity.contentResolver.openFileDescriptor(uri, "rw")
|
||||||
|
descriptor?.use { d ->
|
||||||
|
installState.value = FirmwareInstallState.Install
|
||||||
|
thread {
|
||||||
|
try {
|
||||||
|
RyujinxNative.instance.deviceInstallFirmware(
|
||||||
|
d.fd,
|
||||||
|
extension == "xci"
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
MainActivity.mainViewModel?.refreshFirmwareVersion()
|
||||||
|
installState.value = FirmwareInstallState.Done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearFirmwareSelection(installState: MutableState<FirmwareInstallState>){
|
||||||
|
selectedFirmwareFile = null
|
||||||
|
selectedFirmwareVersion = ""
|
||||||
|
installState.value = FirmwareInstallState.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class FirmwareInstallState{
|
||||||
|
None,
|
||||||
|
Cancelled,
|
||||||
|
Verifying,
|
||||||
|
Query,
|
||||||
|
Install,
|
||||||
|
Done
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@ import com.anggrayudi.storage.file.extension
|
|||||||
import org.ryujinx.android.Helpers
|
import org.ryujinx.android.Helpers
|
||||||
import org.ryujinx.android.MainActivity
|
import org.ryujinx.android.MainActivity
|
||||||
import org.ryujinx.android.providers.DocumentProvider
|
import org.ryujinx.android.providers.DocumentProvider
|
||||||
|
import org.ryujinx.android.viewmodels.FirmwareInstallState
|
||||||
import org.ryujinx.android.viewmodels.MainViewModel
|
import org.ryujinx.android.viewmodels.MainViewModel
|
||||||
import org.ryujinx.android.viewmodels.SettingsViewModel
|
import org.ryujinx.android.viewmodels.SettingsViewModel
|
||||||
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
||||||
@ -107,6 +108,15 @@ class SettingViews {
|
|||||||
val useVirtualController = remember {
|
val useVirtualController = remember {
|
||||||
mutableStateOf(true)
|
mutableStateOf(true)
|
||||||
}
|
}
|
||||||
|
val showFirwmareDialog = remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
val firmwareInstallState = remember {
|
||||||
|
mutableStateOf(FirmwareInstallState.None)
|
||||||
|
}
|
||||||
|
val firmwareVersion = remember {
|
||||||
|
mutableStateOf(mainViewModel.firmwareVersion)
|
||||||
|
}
|
||||||
val isGrid = remember { mutableStateOf(true) }
|
val isGrid = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
val enableDebugLogs = remember { mutableStateOf(true) }
|
val enableDebugLogs = remember { mutableStateOf(true) }
|
||||||
@ -211,36 +221,186 @@ class SettingViews {
|
|||||||
Text(text = "Choose Folder")
|
Text(text = "Choose Folder")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button(onClick = {
|
|
||||||
fun createIntent(action: String) : Intent{
|
Row(
|
||||||
val intent = Intent(action)
|
modifier = Modifier
|
||||||
intent.addCategory(Intent.CATEGORY_DEFAULT)
|
.fillMaxWidth()
|
||||||
intent.data = DocumentsContract.buildRootUri(DocumentProvider.AUTHORITY, DocumentProvider.ROOT_ID)
|
.padding(8.dp),
|
||||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
return intent
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "System Firmware",
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = firmwareVersion.value,
|
||||||
|
modifier = Modifier.align(Alignment.CenterVertically)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
fun createIntent(action: String): Intent {
|
||||||
|
val intent = Intent(action)
|
||||||
|
intent.addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
|
intent.data = DocumentsContract.buildRootUri(
|
||||||
|
DocumentProvider.AUTHORITY,
|
||||||
|
DocumentProvider.ROOT_ID
|
||||||
|
)
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mainViewModel.activity.startActivity(createIntent(Intent.ACTION_VIEW))
|
||||||
|
return@Button
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mainViewModel.activity.startActivity(createIntent("android.provider.action.BROWSE"))
|
||||||
|
return@Button
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mainViewModel.activity.startActivity(createIntent("com.google.android.documentsui"))
|
||||||
|
return@Button
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mainViewModel.activity.startActivity(createIntent("com.android.documentsui"))
|
||||||
|
return@Button
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(text = "Open App Folder")
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
mainViewModel.activity.startActivity(createIntent(Intent.ACTION_VIEW))
|
Button(onClick = {
|
||||||
return@Button
|
settingsViewModel.importProdKeys()
|
||||||
|
}) {
|
||||||
|
Text(text = "Import prod Keys")
|
||||||
}
|
}
|
||||||
catch (_: ActivityNotFoundException){}
|
|
||||||
try {
|
Button(onClick = {
|
||||||
mainViewModel.activity.startActivity(createIntent("android.provider.action.BROWSE"))
|
showFirwmareDialog.value = true
|
||||||
return@Button
|
}) {
|
||||||
|
Text(text = "Install Firmware")
|
||||||
}
|
}
|
||||||
catch (_: ActivityNotFoundException){}
|
}
|
||||||
try {
|
}
|
||||||
mainViewModel.activity.startActivity(createIntent("com.google.android.documentsui"))
|
}
|
||||||
return@Button
|
|
||||||
|
if(showFirwmareDialog.value) {
|
||||||
|
AlertDialog(onDismissRequest = {
|
||||||
|
if(firmwareInstallState.value != FirmwareInstallState.Install) {
|
||||||
|
showFirwmareDialog.value = false
|
||||||
|
settingsViewModel.clearFirmwareSelection(firmwareInstallState)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.CenterHorizontally),
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
if (firmwareInstallState.value == FirmwareInstallState.None) {
|
||||||
|
Text(text = "Select a zip or XCI file to install from.")
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
settingsViewModel.selectFirmware(
|
||||||
|
firmwareInstallState
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
Text(text = "Select File")
|
||||||
|
}
|
||||||
|
Button(onClick = {
|
||||||
|
showFirwmareDialog.value = false
|
||||||
|
settingsViewModel.clearFirmwareSelection(
|
||||||
|
firmwareInstallState
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
Text(text = "Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (firmwareInstallState.value == FirmwareInstallState.Query) {
|
||||||
|
Text(text = "Firmware ${settingsViewModel.selectedFirmwareVersion} will be installed. Do you want to continue?")
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
settingsViewModel.installFirmware(
|
||||||
|
firmwareInstallState
|
||||||
|
)
|
||||||
|
|
||||||
|
if(firmwareInstallState.value == FirmwareInstallState.None){
|
||||||
|
showFirwmareDialog.value = false
|
||||||
|
settingsViewModel.clearFirmwareSelection(firmwareInstallState)
|
||||||
|
}
|
||||||
|
}, modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
Text(text = "Yes")
|
||||||
|
}
|
||||||
|
Button(onClick = {
|
||||||
|
showFirwmareDialog.value = false
|
||||||
|
settingsViewModel.clearFirmwareSelection(
|
||||||
|
firmwareInstallState
|
||||||
|
)
|
||||||
|
}, modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
|
Text(text = "No")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (firmwareInstallState.value == FirmwareInstallState.Install) {
|
||||||
|
Text(text = "Installing Firmware ${settingsViewModel.selectedFirmwareVersion}...")
|
||||||
|
LinearProgressIndicator(modifier = Modifier
|
||||||
|
.padding(top = 4.dp))
|
||||||
|
} else if (firmwareInstallState.value == FirmwareInstallState.Verifying) {
|
||||||
|
Text(text = "Verifying selected file...")
|
||||||
|
LinearProgressIndicator(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else if (firmwareInstallState.value == FirmwareInstallState.Done) {
|
||||||
|
Text(text = "Installed Firmware ${settingsViewModel.selectedFirmwareVersion}")
|
||||||
|
firmwareVersion.value = mainViewModel.firmwareVersion
|
||||||
|
}
|
||||||
|
else if(firmwareInstallState.value == FirmwareInstallState.Cancelled){
|
||||||
|
val file = settingsViewModel.selectedFirmwareFile
|
||||||
|
if(file != null){
|
||||||
|
if(file.extension == "xci" || file.extension == "zip"){
|
||||||
|
if(settingsViewModel.selectedFirmwareVersion.isEmpty()) {
|
||||||
|
Text(text = "Unable to find version in selected file")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text(text = "Unknown Error has occurred. Please check logs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text(text = "File type is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Text(text = "File type is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (_: ActivityNotFoundException){}
|
|
||||||
try {
|
|
||||||
mainViewModel.activity.startActivity(createIntent("com.android.documentsui"))
|
|
||||||
return@Button
|
|
||||||
}
|
|
||||||
catch (_: ActivityNotFoundException){}
|
|
||||||
}) {
|
|
||||||
Text(text = "Open App Folder")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user