android - add support for nro

This commit is contained in:
Emmanuel Hansen 2023-12-16 13:33:40 +00:00
parent 9680ecd820
commit 521403b0ea
9 changed files with 219 additions and 124 deletions

View File

@ -237,7 +237,7 @@ namespace LibRyujinx
}
[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");
if (SwitchDevice?.EmulationContext == null)
@ -247,7 +247,7 @@ namespace LibRyujinx
var stream = OpenFile(descriptor);
return LoadApplication(stream, isXci);
return LoadApplication(stream, (FileType)(int)type);
}
[UnmanagedCallersOnly(EntryPoint = "Java_org_ryujinx_android_RyujinxNative_graphicsInitialize")]
@ -429,12 +429,12 @@ namespace LibRyujinx
}
[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");
using var stream = OpenFile(fileDescriptor);
var info = GetGameInfo(stream, isXci);
var ext = GetStoredString(extension);
var info = GetGameInfo(stream, ext.ToLower());
return GetInfo(jEnv, info);
}

View File

@ -1,4 +1,4 @@
using ARMeilleure.Translation;
using ARMeilleure.Translation;
using LibHac.Ncm;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
@ -66,10 +66,16 @@ namespace LibRyujinx
return LoadApplication(path);
}
public static bool LoadApplication(Stream stream, bool isXci)
public static bool LoadApplication(Stream stream, FileType type)
{
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()
@ -221,5 +227,13 @@ namespace LibRyujinx
Renderer = null;
}
}
public enum FileType
{
None,
Nsp,
Xci,
Nro
}
}
}

View File

@ -34,6 +34,9 @@ using System.Globalization;
using Ryujinx.Ui.Common.Configuration.System;
using Ryujinx.Common.Logging.Targets;
using System.Collections.Generic;
using LibHac.Bcat;
using Ryujinx.Ui.App.Common;
using System.Text;
namespace LibRyujinx
{
@ -126,10 +129,10 @@ namespace LibRyujinx
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)
{
@ -142,7 +145,7 @@ namespace LibRyujinx
FileSize = gameStream.Length * 0.000000000931, TitleName = "Unknown", TitleId = "0000000000000000",
Developer = "Unknown",
Version = "0",
Icon = null,
Icon = null
};
const Language TitleLanguage = Language.AmericanEnglish;
@ -152,12 +155,14 @@ namespace LibRyujinx
try
{
try
{
if (extension == "nsp" || extension == "pfs0" || extension == "xci")
{
IFileSystem pfs;
bool isExeFs = false;
if (isXci)
if (extension == "xci")
{
Xci xci = new(SwitchDevice.VirtualFileSystem.KeySet, gameStream.AsStorage());
@ -279,6 +284,44 @@ namespace LibRyujinx
}
}
}
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);
}
}
}
catch (MissingKeyException exception)
{
Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");

View File

@ -33,9 +33,9 @@ class RyujinxNative {
external fun deviceGetGameFrameRate(): Double
external fun deviceGetGameFrameTime(): 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 deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean
external fun deviceLoadDescriptor(fileDescriptor: Int, gameType: Int): Boolean
external fun graphicsRendererSetSize(width: Int, height: Int)
external fun graphicsRendererSetVsync(enabled: Boolean)
external fun graphicsRendererRunLoop()

View File

@ -4,10 +4,12 @@ import android.content.Context
import android.os.ParcelFileDescriptor
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.extension
import org.ryujinx.android.NativeHelpers
import org.ryujinx.android.RyujinxNative
class GameModel(var file: DocumentFile, val context: Context) {
var type: FileType
var descriptor: ParcelFileDescriptor? = null
var fileName: String?
var fileSize = 0.0
@ -19,8 +21,9 @@ class GameModel(var file: DocumentFile, val context: Context) {
init {
fileName = file.name
var pid = open()
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, file.extension.contains("xci"))
val pid = open()
val ext = NativeHelpers.instance.storeStringJava(file.extension)
val gameInfo = RyujinxNative.instance.deviceGetGameInfo(pid, ext)
close()
fileSize = gameInfo.FileSize
@ -29,6 +32,16 @@ class GameModel(var file: DocumentFile, val context: Context) {
developer = gameInfo.Developer
version = gameInfo.Version
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 {
@ -41,10 +54,6 @@ class GameModel(var file: DocumentFile, val context: Context) {
descriptor?.close()
descriptor = null
}
fun isXci() : Boolean {
return file.extension == "xci"
}
}
class GameInfo {
@ -55,3 +64,10 @@ class GameInfo {
var Version: String? = null
var Icon: String? = null
}
enum class FileType{
None,
Nsp,
Xci,
Nro
}

View File

@ -72,7 +72,7 @@ class HomeViewModel(
loadedCache.clear()
val files = mutableListOf<GameModel>()
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 {
val item = GameModel(file, it)

View File

@ -165,7 +165,7 @@ class MainViewModel(val activity: MainActivity) {
if (!success)
return false
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.isXci())
success = nativeRyujinx.deviceLoadDescriptor(descriptor, game.type.ordinal)
if (!success)
return false

View File

@ -57,11 +57,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
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.HomeViewModel
import org.ryujinx.android.viewmodels.QuickSettings
@ -398,7 +401,7 @@ class HomeViews {
selected = 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 {
showLoading.value = true
val success =
@ -427,7 +430,7 @@ class HomeViews {
horizontalArrangement = Arrangement.SpaceBetween
) {
Row {
if (!gameModel.titleId.isNullOrEmpty() && gameModel.titleId != "0000000000000000") {
if (!gameModel.titleId.isNullOrEmpty() && (gameModel.titleId != "0000000000000000" || gameModel.type == FileType.Nro)) {
if (gameModel.icon?.isNotEmpty() == true) {
val pic = decoder.decode(gameModel.icon)
val size =
@ -441,7 +444,9 @@ class HomeViews {
.width(size.roundToInt().dp)
.height(size.roundToInt().dp)
)
} else NotAvailableIcon()
} else if (gameModel.type == FileType.Nro)
NROIcon()
else NotAvailableIcon()
} else NotAvailableIcon()
Column {
Text(text = gameModel.titleName ?: "")
@ -487,7 +492,7 @@ class HomeViews {
selected = 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 {
showLoading.value = true
val success =
@ -510,7 +515,7 @@ class HomeViews {
})
) {
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) {
val pic = decoder.decode(gameModel.icon)
val size = GridImageSize / Resources.getSystem().displayMetrics.density
@ -523,20 +528,24 @@ class HomeViews {
.clip(RoundedCornerShape(16.dp))
.align(Alignment.CenterHorizontally)
)
} else NotAvailableIcon()
} else if (gameModel.type == FileType.Nro)
NROIcon()
else NotAvailableIcon()
} else NotAvailableIcon()
Text(
text = gameModel.titleName ?: "N/A",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(vertical = 4.dp)
modifier = Modifier
.padding(vertical = 4.dp)
.basicMarquee()
)
Text(
text = gameModel.developer ?: "N/A",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(vertical = 4.dp)
modifier = Modifier
.padding(vertical = 4.dp)
.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB