forked from MeloNX/MeloNX
android - add support for nro
This commit is contained in:
parent
9680ecd820
commit
521403b0ea
@ -237,7 +237,7 @@ namespace LibRyujinx
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceLoadDescriptor")]
|
||||||
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JBoolean isXci)
|
public static JBoolean JniLoadApplicationNative(JEnvRef jEnv, JObjectLocalRef jObj, JInt descriptor, JInt type)
|
||||||
{
|
{
|
||||||
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
if (SwitchDevice?.EmulationContext == null)
|
if (SwitchDevice?.EmulationContext == null)
|
||||||
@ -247,7 +247,7 @@ namespace LibRyujinx
|
|||||||
|
|
||||||
var stream = OpenFile(descriptor);
|
var stream = OpenFile(descriptor);
|
||||||
|
|
||||||
return LoadApplication(stream, isXci);
|
return LoadApplication(stream, (FileType)(int)type);
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
|
||||||
@ -429,12 +429,12 @@ namespace LibRyujinx
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")]
|
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_deviceGetGameInfo")]
|
||||||
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JBoolean isXci)
|
public static JObjectLocalRef JniGetGameInfo(JEnvRef jEnv, JObjectLocalRef jObj, JInt fileDescriptor, JLong extension)
|
||||||
{
|
{
|
||||||
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
Logger.Trace?.Print(LogClass.Application, "Jni Function Call");
|
||||||
using var stream = OpenFile(fileDescriptor);
|
using var stream = OpenFile(fileDescriptor);
|
||||||
|
var ext = GetStoredString(extension);
|
||||||
var info = GetGameInfo(stream, isXci);
|
var info = GetGameInfo(stream, ext.ToLower());
|
||||||
return GetInfo(jEnv, info);
|
return GetInfo(jEnv, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using ARMeilleure.Translation;
|
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;
|
||||||
@ -66,10 +66,16 @@ namespace LibRyujinx
|
|||||||
return LoadApplication(path);
|
return LoadApplication(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool LoadApplication(Stream stream, bool isXci)
|
public static bool LoadApplication(Stream stream, FileType type)
|
||||||
{
|
{
|
||||||
var emulationContext = SwitchDevice.EmulationContext;
|
var emulationContext = SwitchDevice.EmulationContext;
|
||||||
return (isXci ? emulationContext?.LoadXci(stream) : emulationContext.LoadNsp(stream)) ?? false;
|
return type switch
|
||||||
|
{
|
||||||
|
FileType.None => false,
|
||||||
|
FileType.Nsp => emulationContext?.LoadNsp(stream) ?? false,
|
||||||
|
FileType.Xci => emulationContext?.LoadXci(stream) ?? false,
|
||||||
|
FileType.Nro => emulationContext?.LoadProgram(stream, true, "") ?? false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool LaunchMiiEditApplet()
|
public static bool LaunchMiiEditApplet()
|
||||||
@ -221,5 +227,13 @@ namespace LibRyujinx
|
|||||||
Renderer = null;
|
Renderer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum FileType
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Nsp,
|
||||||
|
Xci,
|
||||||
|
Nro
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,9 @@ using System.Globalization;
|
|||||||
using Ryujinx.Ui.Common.Configuration.System;
|
using Ryujinx.Ui.Common.Configuration.System;
|
||||||
using Ryujinx.Common.Logging.Targets;
|
using Ryujinx.Common.Logging.Targets;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using LibHac.Bcat;
|
||||||
|
using Ryujinx.Ui.App.Common;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace LibRyujinx
|
namespace LibRyujinx
|
||||||
{
|
{
|
||||||
@ -126,10 +129,10 @@ namespace LibRyujinx
|
|||||||
|
|
||||||
using var stream = File.Open(file, FileMode.Open);
|
using var stream = File.Open(file, FileMode.Open);
|
||||||
|
|
||||||
return GetGameInfo(stream, file.ToLower().EndsWith("xci"));
|
return GetGameInfo(stream, new FileInfo(file).Extension.Remove('.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GameInfo? GetGameInfo(Stream gameStream, bool isXci)
|
public static GameInfo? GetGameInfo(Stream gameStream, string extension)
|
||||||
{
|
{
|
||||||
if (SwitchDevice == null)
|
if (SwitchDevice == null)
|
||||||
{
|
{
|
||||||
@ -142,7 +145,7 @@ namespace LibRyujinx
|
|||||||
FileSize = gameStream.Length * 0.000000000931, TitleName = "Unknown", TitleId = "0000000000000000",
|
FileSize = gameStream.Length * 0.000000000931, TitleName = "Unknown", TitleId = "0000000000000000",
|
||||||
Developer = "Unknown",
|
Developer = "Unknown",
|
||||||
Version = "0",
|
Version = "0",
|
||||||
Icon = null,
|
Icon = null
|
||||||
};
|
};
|
||||||
|
|
||||||
const Language TitleLanguage = Language.AmericanEnglish;
|
const Language TitleLanguage = Language.AmericanEnglish;
|
||||||
@ -153,129 +156,169 @@ namespace LibRyujinx
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IFileSystem pfs;
|
if (extension == "nsp" || extension == "pfs0" || extension == "xci")
|
||||||
|
|
||||||
bool isExeFs = false;
|
|
||||||
|
|
||||||
if (isXci)
|
|
||||||
{
|
{
|
||||||
Xci xci = new(SwitchDevice.VirtualFileSystem.KeySet, gameStream.AsStorage());
|
IFileSystem pfs;
|
||||||
|
|
||||||
pfs = xci.OpenPartition(XciPartitionType.Secure);
|
bool isExeFs = false;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var pfsTemp = new PartitionFileSystem();
|
|
||||||
pfsTemp.Initialize(gameStream.AsStorage()).ThrowIfFailure();
|
|
||||||
pfs = pfsTemp;
|
|
||||||
|
|
||||||
// If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
|
if (extension == "xci")
|
||||||
bool hasMainNca = false;
|
|
||||||
|
|
||||||
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
|
|
||||||
{
|
{
|
||||||
if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
|
Xci xci = new(SwitchDevice.VirtualFileSystem.KeySet, gameStream.AsStorage());
|
||||||
|
|
||||||
|
pfs = xci.OpenPartition(XciPartitionType.Secure);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var pfsTemp = new PartitionFileSystem();
|
||||||
|
pfsTemp.Initialize(gameStream.AsStorage()).ThrowIfFailure();
|
||||||
|
pfs = pfsTemp;
|
||||||
|
|
||||||
|
// If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
|
||||||
|
bool hasMainNca = false;
|
||||||
|
|
||||||
|
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
|
||||||
{
|
{
|
||||||
using UniqueRef<IFile> ncaFile = new();
|
if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
|
||||||
|
|
||||||
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
Nca nca = new(SwitchDevice.VirtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
|
||||||
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
|
||||||
|
|
||||||
// Some main NCAs don't have a data partition, so check if the partition exists before opening it
|
|
||||||
if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
|
|
||||||
{
|
{
|
||||||
hasMainNca = true;
|
using UniqueRef<IFile> ncaFile = new();
|
||||||
|
|
||||||
break;
|
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
Nca nca = new(SwitchDevice.VirtualFileSystem.KeySet, ncaFile.Get.AsStorage());
|
||||||
|
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
|
||||||
|
|
||||||
|
// Some main NCAs don't have a data partition, so check if the partition exists before opening it
|
||||||
|
if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
|
||||||
|
{
|
||||||
|
hasMainNca = true;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
|
||||||
|
{
|
||||||
|
isExeFs = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
|
|
||||||
|
if (!hasMainNca && !isExeFs)
|
||||||
{
|
{
|
||||||
isExeFs = true;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasMainNca && !isExeFs)
|
if (isExeFs)
|
||||||
{
|
{
|
||||||
return null;
|
using UniqueRef<IFile> npdmFile = new();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isExeFs)
|
Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
|
||||||
{
|
|
||||||
using UniqueRef<IFile> npdmFile = new();
|
|
||||||
|
|
||||||
Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
|
if (ResultFs.PathNotFound.Includes(result))
|
||||||
|
|
||||||
if (ResultFs.PathNotFound.Includes(result))
|
|
||||||
{
|
|
||||||
Npdm npdm = new(npdmFile.Get.AsStream());
|
|
||||||
|
|
||||||
gameInfo.TitleName = npdm.TitleName;
|
|
||||||
gameInfo.TitleId = npdm.Aci0.TitleId.ToString("x16");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
GetControlFsAndTitleId(pfs, out IFileSystem? controlFs, out string? id);
|
|
||||||
|
|
||||||
gameInfo.TitleId = id;
|
|
||||||
|
|
||||||
if (controlFs == null)
|
|
||||||
{
|
|
||||||
Logger.Error?.Print(LogClass.Application, $"No control FS was returned. Unable to process game any further: {gameInfo.TitleName}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there is an update available.
|
|
||||||
if (IsUpdateApplied(gameInfo.TitleId, out IFileSystem? updatedControlFs))
|
|
||||||
{
|
|
||||||
// Replace the original ControlFs by the updated one.
|
|
||||||
controlFs = updatedControlFs;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadControlData(controlFs, controlHolder.ByteSpan);
|
|
||||||
|
|
||||||
GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out _, out gameInfo.Developer, out gameInfo.Version);
|
|
||||||
|
|
||||||
// Read the icon from the ControlFS and store it as a byte array
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using UniqueRef<IFile> icon = new();
|
|
||||||
|
|
||||||
controlFs?.OpenFile(ref icon.Ref, $"/icon_{TitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
|
||||||
|
|
||||||
using MemoryStream stream = new();
|
|
||||||
|
|
||||||
icon.Get.AsStream().CopyTo(stream);
|
|
||||||
gameInfo.Icon = stream.ToArray();
|
|
||||||
}
|
|
||||||
catch (HorizonResultException)
|
|
||||||
{
|
|
||||||
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
|
|
||||||
{
|
{
|
||||||
if (entry.Name == "control.nacp")
|
Npdm npdm = new(npdmFile.Get.AsStream());
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var icon = new UniqueRef<IFile>();
|
gameInfo.TitleName = npdm.TitleName;
|
||||||
|
gameInfo.TitleId = npdm.Aci0.TitleId.ToString("x16");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GetControlFsAndTitleId(pfs, out IFileSystem? controlFs, out string? id);
|
||||||
|
|
||||||
controlFs?.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
gameInfo.TitleId = id;
|
||||||
|
|
||||||
|
if (controlFs == null)
|
||||||
|
{
|
||||||
|
Logger.Error?.Print(LogClass.Application, $"No control FS was returned. Unable to process game any further: {gameInfo.TitleName}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is an update available.
|
||||||
|
if (IsUpdateApplied(gameInfo.TitleId, out IFileSystem? updatedControlFs))
|
||||||
|
{
|
||||||
|
// Replace the original ControlFs by the updated one.
|
||||||
|
controlFs = updatedControlFs;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadControlData(controlFs, controlHolder.ByteSpan);
|
||||||
|
|
||||||
|
GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out _, out gameInfo.Developer, out gameInfo.Version);
|
||||||
|
|
||||||
|
// Read the icon from the ControlFS and store it as a byte array
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using UniqueRef<IFile> icon = new();
|
||||||
|
|
||||||
|
controlFs?.OpenFile(ref icon.Ref, $"/icon_{TitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
using MemoryStream stream = new();
|
using MemoryStream stream = new();
|
||||||
|
|
||||||
icon.Get.AsStream().CopyTo(stream);
|
icon.Get.AsStream().CopyTo(stream);
|
||||||
gameInfo.Icon = stream.ToArray();
|
gameInfo.Icon = stream.ToArray();
|
||||||
|
}
|
||||||
if (gameInfo.Icon != null)
|
catch (HorizonResultException)
|
||||||
|
{
|
||||||
|
foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
|
||||||
{
|
{
|
||||||
break;
|
if (entry.Name == "control.nacp")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var icon = new UniqueRef<IFile>();
|
||||||
|
|
||||||
|
controlFs?.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||||
|
|
||||||
|
using MemoryStream stream = new();
|
||||||
|
|
||||||
|
icon.Get.AsStream().CopyTo(stream);
|
||||||
|
gameInfo.Icon = stream.ToArray();
|
||||||
|
|
||||||
|
if (gameInfo.Icon != null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (extension == "nro")
|
||||||
|
{
|
||||||
|
BinaryReader reader = new(gameStream);
|
||||||
|
|
||||||
|
byte[] Read(long position, int size)
|
||||||
|
{
|
||||||
|
gameStream.Seek(position, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
return reader.ReadBytes(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
gameStream.Seek(24, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
int assetOffset = reader.ReadInt32();
|
||||||
|
|
||||||
|
if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
|
||||||
|
{
|
||||||
|
byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
|
||||||
|
|
||||||
|
long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
|
||||||
|
long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
|
||||||
|
|
||||||
|
ulong nacpOffset = reader.ReadUInt64();
|
||||||
|
ulong nacpSize = reader.ReadUInt64();
|
||||||
|
|
||||||
|
// Reads and stores game icon as byte array
|
||||||
|
if (iconSize > 0)
|
||||||
|
{
|
||||||
|
gameInfo.Icon = Read(assetOffset + iconOffset, (int)iconSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read the NACP data
|
||||||
|
Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan);
|
||||||
|
|
||||||
|
GetGameInformation(ref controlHolder.Value, out gameInfo.TitleName, out _, out gameInfo.Developer, out gameInfo.Version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,9 +33,9 @@ class RyujinxNative {
|
|||||||
external fun deviceGetGameFrameRate(): Double
|
external fun deviceGetGameFrameRate(): Double
|
||||||
external fun deviceGetGameFrameTime(): Double
|
external fun deviceGetGameFrameTime(): Double
|
||||||
external fun deviceGetGameFifo(): Double
|
external fun deviceGetGameFifo(): Double
|
||||||
external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): 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, isXci:Boolean): 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()
|
||||||
|
@ -4,10 +4,12 @@ import android.content.Context
|
|||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.anggrayudi.storage.file.extension
|
import com.anggrayudi.storage.file.extension
|
||||||
|
import org.ryujinx.android.NativeHelpers
|
||||||
import org.ryujinx.android.RyujinxNative
|
import org.ryujinx.android.RyujinxNative
|
||||||
|
|
||||||
|
|
||||||
class GameModel(var file: DocumentFile, val context: Context) {
|
class GameModel(var file: DocumentFile, val context: Context) {
|
||||||
|
var type: FileType
|
||||||
var descriptor: ParcelFileDescriptor? = null
|
var descriptor: ParcelFileDescriptor? = null
|
||||||
var fileName: String?
|
var fileName: String?
|
||||||
var fileSize = 0.0
|
var fileSize = 0.0
|
||||||
@ -19,8 +21,9 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
fileName = file.name
|
fileName = file.name
|
||||||
var pid = open()
|
val pid = open()
|
||||||
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, file.extension.contains("xci"))
|
val ext = NativeHelpers.instance.storeStringJava(file.extension)
|
||||||
|
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, ext)
|
||||||
close()
|
close()
|
||||||
|
|
||||||
fileSize = gameInfo.FileSize
|
fileSize = gameInfo.FileSize
|
||||||
@ -29,6 +32,16 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
|||||||
developer = gameInfo.Developer
|
developer = gameInfo.Developer
|
||||||
version = gameInfo.Version
|
version = gameInfo.Version
|
||||||
icon = gameInfo.Icon
|
icon = gameInfo.Icon
|
||||||
|
type = when {
|
||||||
|
(file.extension == "xci") -> FileType.Xci
|
||||||
|
(file.extension == "nsp") -> FileType.Nsp
|
||||||
|
(file.extension == "nro") -> FileType.Nro
|
||||||
|
else -> FileType.None
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == FileType.Nro && (titleName.isNullOrEmpty() || titleName == "Unknown")) {
|
||||||
|
titleName = file.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun open() : Int {
|
fun open() : Int {
|
||||||
@ -41,10 +54,6 @@ class GameModel(var file: DocumentFile, val context: Context) {
|
|||||||
descriptor?.close()
|
descriptor?.close()
|
||||||
descriptor = null
|
descriptor = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isXci() : Boolean {
|
|
||||||
return file.extension == "xci"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class GameInfo {
|
class GameInfo {
|
||||||
@ -55,3 +64,10 @@ class GameInfo {
|
|||||||
var Version: String? = null
|
var Version: String? = null
|
||||||
var Icon: String? = null
|
var Icon: String? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class FileType{
|
||||||
|
None,
|
||||||
|
Nsp,
|
||||||
|
Xci,
|
||||||
|
Nro
|
||||||
|
}
|
||||||
|
@ -72,7 +72,7 @@ class HomeViewModel(
|
|||||||
loadedCache.clear()
|
loadedCache.clear()
|
||||||
val files = mutableListOf<GameModel>()
|
val files = mutableListOf<GameModel>()
|
||||||
for (file in folder.search(false, DocumentFileType.FILE)) {
|
for (file in folder.search(false, DocumentFileType.FILE)) {
|
||||||
if (file.extension == "xci" || file.extension == "nsp")
|
if (file.extension == "xci" || file.extension == "nsp" || file.extension == "nro")
|
||||||
activity.let {
|
activity.let {
|
||||||
val item = GameModel(file, it)
|
val item = GameModel(file, it)
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ class MainViewModel(val activity: MainActivity) {
|
|||||||
if (!success)
|
if (!success)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.isXci())
|
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal)
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
return false
|
return false
|
||||||
|
@ -57,11 +57,14 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import com.anggrayudi.storage.extension.launchOnUiThread
|
import com.anggrayudi.storage.extension.launchOnUiThread
|
||||||
|
import org.ryujinx.android.R
|
||||||
|
import org.ryujinx.android.viewmodels.FileType
|
||||||
import org.ryujinx.android.viewmodels.GameModel
|
import org.ryujinx.android.viewmodels.GameModel
|
||||||
import org.ryujinx.android.viewmodels.HomeViewModel
|
import org.ryujinx.android.viewmodels.HomeViewModel
|
||||||
import org.ryujinx.android.viewmodels.QuickSettings
|
import org.ryujinx.android.viewmodels.QuickSettings
|
||||||
@ -398,7 +401,7 @@ class HomeViews {
|
|||||||
selected = null
|
selected = null
|
||||||
}
|
}
|
||||||
selectedModel.value = null
|
selectedModel.value = null
|
||||||
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
|
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) {
|
||||||
thread {
|
thread {
|
||||||
showLoading.value = true
|
showLoading.value = true
|
||||||
val success =
|
val success =
|
||||||
@ -427,7 +430,7 @@ class HomeViews {
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
|
if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) {
|
||||||
if (gameModel.icon?.isNotEmpty() == true) {
|
if (gameModel.icon?.isNotEmpty() == true) {
|
||||||
val pic = decoder.decode(gameModel.icon)
|
val pic = decoder.decode(gameModel.icon)
|
||||||
val size =
|
val size =
|
||||||
@ -441,7 +444,9 @@ class HomeViews {
|
|||||||
.width(size.roundToInt().dp)
|
.width(size.roundToInt().dp)
|
||||||
.height(size.roundToInt().dp)
|
.height(size.roundToInt().dp)
|
||||||
)
|
)
|
||||||
} else NotAvailableIcon()
|
} else if (gameModel.type == FileType.Nro)
|
||||||
|
NROIcon()
|
||||||
|
else NotAvailableIcon()
|
||||||
} else NotAvailableIcon()
|
} else NotAvailableIcon()
|
||||||
Column {
|
Column {
|
||||||
Text(text = gameModel.titleName ?: "")
|
Text(text = gameModel.titleName ?: "")
|
||||||
@ -487,7 +492,7 @@ class HomeViews {
|
|||||||
selected = null
|
selected = null
|
||||||
}
|
}
|
||||||
selectedModel.value = null
|
selectedModel.value = null
|
||||||
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000") {
|
} else if (gameModel.titleId.isNullOrEmpty() || gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro) {
|
||||||
thread {
|
thread {
|
||||||
showLoading.value = true
|
showLoading.value = true
|
||||||
val success =
|
val success =
|
||||||
@ -510,7 +515,7 @@ class HomeViews {
|
|||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(4.dp)) {
|
Column(modifier = Modifier.padding(4.dp)) {
|
||||||
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
|
if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) {
|
||||||
if (gameModel.icon?.isNotEmpty() == true) {
|
if (gameModel.icon?.isNotEmpty() == true) {
|
||||||
val pic = decoder.decode(gameModel.icon)
|
val pic = decoder.decode(gameModel.icon)
|
||||||
val size = GridImageSize / Resources.getSystem().displayMetrics.density
|
val size = GridImageSize / Resources.getSystem().displayMetrics.density
|
||||||
@ -523,20 +528,24 @@ class HomeViews {
|
|||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
)
|
)
|
||||||
} else NotAvailableIcon()
|
} else if (gameModel.type == FileType.Nro)
|
||||||
|
NROIcon()
|
||||||
|
else NotAvailableIcon()
|
||||||
} else NotAvailableIcon()
|
} else NotAvailableIcon()
|
||||||
Text(
|
Text(
|
||||||
text = gameModel.titleName ?: "N/A",
|
text = gameModel.titleName ?: "N/A",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
.basicMarquee()
|
.basicMarquee()
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = gameModel.developer ?: "N/A",
|
text = gameModel.developer ?: "N/A",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
.basicMarquee()
|
.basicMarquee()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -556,6 +565,19 @@ class HomeViews {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NROIcon() {
|
||||||
|
val size = ListImageSize / Resources.getSystem().displayMetrics.density
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.icon_nro),
|
||||||
|
contentDescription = "NRO",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.width(size.roundToInt().dp)
|
||||||
|
.height(size.roundToInt().dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
BIN
src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png
Normal file
BIN
src/RyujinxAndroid/app/src/main/res/drawable/icon_nro.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Loading…
x
Reference in New Issue
Block a user