android: Refactor game metadata collection to new file
This also removes irrelevant data and adds new information from/to the Game data class and RomMetadata struct
This commit is contained in:
parent
1e61c3e1e7
commit
a9e29a3972
|
@ -215,32 +215,6 @@ object NativeLibrary {
|
||||||
|
|
||||||
external fun initGameIni(gameID: String?)
|
external fun initGameIni(gameID: String?)
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the embedded icon within the given ROM.
|
|
||||||
*
|
|
||||||
* @param filename the file path to the ROM.
|
|
||||||
* @return a byte array containing the JPEG data for the icon.
|
|
||||||
*/
|
|
||||||
external fun getIcon(filename: String): ByteArray
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the embedded title of the given ISO/ROM.
|
|
||||||
*
|
|
||||||
* @param filename The file path to the ISO/ROM.
|
|
||||||
* @return the embedded title of the ISO/ROM.
|
|
||||||
*/
|
|
||||||
external fun getTitle(filename: String): String
|
|
||||||
|
|
||||||
external fun getDescription(filename: String): String
|
|
||||||
|
|
||||||
external fun getGameId(filename: String): String
|
|
||||||
|
|
||||||
external fun getRegions(filename: String): String
|
|
||||||
|
|
||||||
external fun getCompany(filename: String): String
|
|
||||||
|
|
||||||
external fun isHomebrew(filename: String): Boolean
|
|
||||||
|
|
||||||
external fun setAppDirectory(directory: String)
|
external fun setAppDirectory(directory: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -293,11 +267,6 @@ object NativeLibrary {
|
||||||
*/
|
*/
|
||||||
external fun stopEmulation()
|
external fun stopEmulation()
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the in-memory ROM metadata cache.
|
|
||||||
*/
|
|
||||||
external fun resetRomMetadata()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if emulation is running (or is paused).
|
* Returns true if emulation is running (or is paused).
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -147,7 +147,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
||||||
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||||
return oldItem.gameId == newItem.gameId
|
return oldItem.programId == newItem.programId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||||
|
|
|
@ -12,15 +12,14 @@ import kotlinx.serialization.Serializable
|
||||||
@Serializable
|
@Serializable
|
||||||
class Game(
|
class Game(
|
||||||
val title: String,
|
val title: String,
|
||||||
val description: String,
|
|
||||||
val regions: String,
|
|
||||||
val path: String,
|
val path: String,
|
||||||
val gameId: String,
|
val programId: String,
|
||||||
val company: String,
|
val developer: String,
|
||||||
|
val version: String,
|
||||||
val isHomebrew: Boolean
|
val isHomebrew: Boolean
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
val keyAddedToLibraryTime get() = "${gameId}_AddedToLibraryTime"
|
val keyAddedToLibraryTime get() = "${programId}_AddedToLibraryTime"
|
||||||
val keyLastPlayedTime get() = "${gameId}_LastPlayed"
|
val keyLastPlayedTime get() = "${programId}_LastPlayed"
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other !is Game) {
|
if (other !is Game) {
|
||||||
|
@ -32,11 +31,9 @@ class Game(
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = title.hashCode()
|
var result = title.hashCode()
|
||||||
result = 31 * result + description.hashCode()
|
|
||||||
result = 31 * result + regions.hashCode()
|
|
||||||
result = 31 * result + path.hashCode()
|
result = 31 * result + path.hashCode()
|
||||||
result = 31 * result + gameId.hashCode()
|
result = 31 * result + programId.hashCode()
|
||||||
result = 31 * result + company.hashCode()
|
result = 31 * result + developer.hashCode()
|
||||||
result = 31 * result + isHomebrew.hashCode()
|
result = 31 * result + isHomebrew.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,15 +14,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.MissingFieldException
|
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.utils.GameHelper
|
import org.yuzu.yuzu_emu.utils.GameHelper
|
||||||
|
import org.yuzu.yuzu_emu.utils.GameMetadata
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
class GamesViewModel : ViewModel() {
|
class GamesViewModel : ViewModel() {
|
||||||
val games: StateFlow<List<Game>> get() = _games
|
val games: StateFlow<List<Game>> get() = _games
|
||||||
private val _games = MutableStateFlow(emptyList<Game>())
|
private val _games = MutableStateFlow(emptyList<Game>())
|
||||||
|
@ -58,7 +56,8 @@ class GamesViewModel : ViewModel() {
|
||||||
val game: Game
|
val game: Game
|
||||||
try {
|
try {
|
||||||
game = Json.decodeFromString(it)
|
game = Json.decodeFromString(it)
|
||||||
} catch (e: MissingFieldException) {
|
} catch (e: Exception) {
|
||||||
|
// We don't care about any errors related to parsing the game cache
|
||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +112,7 @@ class GamesViewModel : ViewModel() {
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
NativeLibrary.resetRomMetadata()
|
GameMetadata.resetMetadata()
|
||||||
setGames(GameHelper.getGames())
|
setGames(GameHelper.getGames())
|
||||||
_isReloading.value = false
|
_isReloading.value = false
|
||||||
|
|
||||||
|
|
|
@ -71,27 +71,26 @@ object GameHelper {
|
||||||
|
|
||||||
fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
|
fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
|
||||||
val filePath = uri.toString()
|
val filePath = uri.toString()
|
||||||
var name = NativeLibrary.getTitle(filePath)
|
var name = GameMetadata.getTitle(filePath)
|
||||||
|
|
||||||
// If the game's title field is empty, use the filename.
|
// If the game's title field is empty, use the filename.
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
name = FileUtil.getFilename(uri)
|
name = FileUtil.getFilename(uri)
|
||||||
}
|
}
|
||||||
var gameId = NativeLibrary.getGameId(filePath)
|
var programId = GameMetadata.getProgramId(filePath)
|
||||||
|
|
||||||
// If the game's ID field is empty, use the filename without extension.
|
// If the game's ID field is empty, use the filename without extension.
|
||||||
if (gameId.isEmpty()) {
|
if (programId.isEmpty()) {
|
||||||
gameId = name.substring(0, name.lastIndexOf("."))
|
programId = name.substring(0, name.lastIndexOf("."))
|
||||||
}
|
}
|
||||||
|
|
||||||
val newGame = Game(
|
val newGame = Game(
|
||||||
name,
|
name,
|
||||||
NativeLibrary.getDescription(filePath).replace("\n", " "),
|
|
||||||
NativeLibrary.getRegions(filePath),
|
|
||||||
filePath,
|
filePath,
|
||||||
gameId,
|
programId,
|
||||||
NativeLibrary.getCompany(filePath),
|
GameMetadata.getDeveloper(filePath),
|
||||||
NativeLibrary.isHomebrew(filePath)
|
GameMetadata.getVersion(filePath),
|
||||||
|
GameMetadata.getIsHomebrew(filePath)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (addedToLibrary) {
|
if (addedToLibrary) {
|
||||||
|
|
|
@ -18,7 +18,6 @@ import coil.key.Keyer
|
||||||
import coil.memory.MemoryCache
|
import coil.memory.MemoryCache
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.request.Options
|
import coil.request.Options
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary
|
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
|
@ -36,7 +35,7 @@ class GameIconFetcher(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun decodeGameIcon(uri: String): Bitmap? {
|
private fun decodeGameIcon(uri: String): Bitmap? {
|
||||||
val data = NativeLibrary.getIcon(uri)
|
val data = GameMetadata.getIcon(uri)
|
||||||
return BitmapFactory.decodeByteArray(
|
return BitmapFactory.decodeByteArray(
|
||||||
data,
|
data,
|
||||||
0,
|
0,
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
|
object GameMetadata {
|
||||||
|
external fun getTitle(path: String): String
|
||||||
|
|
||||||
|
external fun getProgramId(path: String): String
|
||||||
|
|
||||||
|
external fun getDeveloper(path: String): String
|
||||||
|
|
||||||
|
external fun getVersion(path: String): String
|
||||||
|
|
||||||
|
external fun getIcon(path: String): ByteArray
|
||||||
|
|
||||||
|
external fun getIsHomebrew(path: String): Boolean
|
||||||
|
|
||||||
|
external fun resetMetadata()
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ add_library(yuzu-android SHARED
|
||||||
native.h
|
native.h
|
||||||
native_config.cpp
|
native_config.cpp
|
||||||
uisettings.cpp
|
uisettings.cpp
|
||||||
|
game_metadata.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
|
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
|
||||||
|
|
112
src/android/app/src/main/jni/game_metadata.cpp
Normal file
112
src/android/app/src/main/jni/game_metadata.cpp
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <core/core.h>
|
||||||
|
#include <core/file_sys/patch_manager.h>
|
||||||
|
#include <core/loader/nro.h>
|
||||||
|
#include <jni.h>
|
||||||
|
#include "core/loader/loader.h"
|
||||||
|
#include "jni/android_common/android_common.h"
|
||||||
|
#include "native.h"
|
||||||
|
|
||||||
|
struct RomMetadata {
|
||||||
|
std::string title;
|
||||||
|
u64 programId;
|
||||||
|
std::string developer;
|
||||||
|
std::string version;
|
||||||
|
std::vector<u8> icon;
|
||||||
|
bool isHomebrew;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
|
||||||
|
|
||||||
|
RomMetadata CacheRomMetadata(const std::string& path) {
|
||||||
|
const auto file =
|
||||||
|
Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
|
||||||
|
auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
|
||||||
|
|
||||||
|
RomMetadata entry;
|
||||||
|
loader->ReadTitle(entry.title);
|
||||||
|
loader->ReadProgramId(entry.programId);
|
||||||
|
loader->ReadIcon(entry.icon);
|
||||||
|
|
||||||
|
const FileSys::PatchManager pm{
|
||||||
|
entry.programId, EmulationSession::GetInstance().System().GetFileSystemController(),
|
||||||
|
EmulationSession::GetInstance().System().GetContentProvider()};
|
||||||
|
const auto control = pm.GetControlMetadata();
|
||||||
|
|
||||||
|
if (control.first != nullptr) {
|
||||||
|
entry.developer = control.first->GetDeveloperName();
|
||||||
|
entry.version = control.first->GetVersionString();
|
||||||
|
} else {
|
||||||
|
FileSys::NACP nacp;
|
||||||
|
if (loader->ReadControlData(nacp) == Loader::ResultStatus::Success) {
|
||||||
|
entry.developer = nacp.GetDeveloperName();
|
||||||
|
} else {
|
||||||
|
entry.developer = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.version = "1.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loader->GetFileType() == Loader::FileType::NRO) {
|
||||||
|
auto loader_nro = reinterpret_cast<Loader::AppLoader_NRO*>(loader.get());
|
||||||
|
entry.isHomebrew = loader_nro->IsHomebrew();
|
||||||
|
} else {
|
||||||
|
entry.isHomebrew = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_rom_metadata_cache[path] = entry;
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
RomMetadata GetRomMetadata(const std::string& path) {
|
||||||
|
if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
|
||||||
|
return search->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CacheRomMetadata(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj,
|
||||||
|
jstring jpath) {
|
||||||
|
return ToJString(env, GetRomMetadata(GetJString(env, jpath)).title);
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getProgramId(JNIEnv* env, jobject obj,
|
||||||
|
jstring jpath) {
|
||||||
|
return ToJString(env, std::to_string(GetRomMetadata(GetJString(env, jpath)).programId));
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getDeveloper(JNIEnv* env, jobject obj,
|
||||||
|
jstring jpath) {
|
||||||
|
return ToJString(env, GetRomMetadata(GetJString(env, jpath)).developer);
|
||||||
|
}
|
||||||
|
|
||||||
|
jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getVersion(JNIEnv* env, jobject obj,
|
||||||
|
jstring jpath) {
|
||||||
|
return ToJString(env, GetRomMetadata(GetJString(env, jpath)).version);
|
||||||
|
}
|
||||||
|
|
||||||
|
jbyteArray Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIcon(JNIEnv* env, jobject obj,
|
||||||
|
jstring jpath) {
|
||||||
|
auto icon_data = GetRomMetadata(GetJString(env, jpath)).icon;
|
||||||
|
jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size()));
|
||||||
|
env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon),
|
||||||
|
reinterpret_cast<jbyte*>(icon_data.data()));
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsHomebrew(JNIEnv* env, jobject obj,
|
||||||
|
jstring jpath) {
|
||||||
|
return static_cast<jboolean>(GetRomMetadata(GetJString(env, jpath)).isHomebrew);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_utils_GameMetadata_resetMetadata(JNIEnv* env, jobject obj) {
|
||||||
|
return m_rom_metadata_cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
|
@ -558,10 +558,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_stopEmulation(JNIEnv* env, jclass cla
|
||||||
EmulationSession::GetInstance().HaltEmulation();
|
EmulationSession::GetInstance().HaltEmulation();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_resetRomMetadata(JNIEnv* env, jclass clazz) {
|
|
||||||
EmulationSession::GetInstance().ResetRomMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning(JNIEnv* env, jclass clazz) {
|
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning(JNIEnv* env, jclass clazz) {
|
||||||
return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
|
return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
|
||||||
}
|
}
|
||||||
|
@ -667,46 +663,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getIcon(JNIEnv* env, jclass clazz,
|
|
||||||
jstring j_filename) {
|
|
||||||
jauto icon_data = EmulationSession::GetInstance().GetRomIcon(GetJString(env, j_filename));
|
|
||||||
jbyteArray icon = env->NewByteArray(static_cast<jsize>(icon_data.size()));
|
|
||||||
env->SetByteArrayRegion(icon, 0, env->GetArrayLength(icon),
|
|
||||||
reinterpret_cast<jbyte*>(icon_data.data()));
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getTitle(JNIEnv* env, jclass clazz,
|
|
||||||
jstring j_filename) {
|
|
||||||
jauto title = EmulationSession::GetInstance().GetRomTitle(GetJString(env, j_filename));
|
|
||||||
return env->NewStringUTF(title.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getDescription(JNIEnv* env, jclass clazz,
|
|
||||||
jstring j_filename) {
|
|
||||||
return j_filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGameId(JNIEnv* env, jclass clazz,
|
|
||||||
jstring j_filename) {
|
|
||||||
return j_filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getRegions(JNIEnv* env, jclass clazz,
|
|
||||||
jstring j_filename) {
|
|
||||||
return env->NewStringUTF("");
|
|
||||||
}
|
|
||||||
|
|
||||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCompany(JNIEnv* env, jclass clazz,
|
|
||||||
jstring j_filename) {
|
|
||||||
return env->NewStringUTF("");
|
|
||||||
}
|
|
||||||
|
|
||||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHomebrew(JNIEnv* env, jclass clazz,
|
|
||||||
jstring j_filename) {
|
|
||||||
return EmulationSession::GetInstance().GetIsHomebrew(GetJString(env, j_filename));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation(JNIEnv* env, jclass clazz) {
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmulation(JNIEnv* env, jclass clazz) {
|
||||||
// Create the default config.ini.
|
// Create the default config.ini.
|
||||||
Config{};
|
Config{};
|
||||||
|
|
Loading…
Reference in a new issue