Add NES emulation support with bundled FCEUmm core
Implement NesCore with FCEUmm/Nestopia/Mesen libretro core support. Generalize native frontend to handle cores that require ROM data in memory (need_fullpath=false) and move PS1-specific config out of C++ into Ps1Core. Bundle both core .so files as APK assets with automatic extraction. Fix cover art fetching for GoodNES-named ROMs by expanding region codes (U)->(USA, Europe) to match LibRetro thumbnail naming.
This commit is contained in:
BIN
app/src/main/assets/cores/fceumm_libretro_android.so
Executable file
BIN
app/src/main/assets/cores/fceumm_libretro_android.so
Executable file
Binary file not shown.
BIN
app/src/main/assets/cores/pcsx_rearmed_libretro_android.so
Executable file
BIN
app/src/main/assets/cores/pcsx_rearmed_libretro_android.so
Executable file
Binary file not shown.
@@ -121,4 +121,18 @@ Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetSaveDir(JNIEnv* env, job
|
|||||||
env->ReleaseStringUTFChars(dir, d);
|
env->ReleaseStringUTFChars(dir, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetControllerPortDevice(JNIEnv*, jobject, jint port, jint device) {
|
||||||
|
LibretroFrontend::instance().setControllerPortDevice(static_cast<unsigned>(port), static_cast<unsigned>(device));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetCoreOption(JNIEnv* env, jobject, jstring key, jstring value) {
|
||||||
|
const char* k = env->GetStringUTFChars(key, nullptr);
|
||||||
|
const char* v = env->GetStringUTFChars(value, nullptr);
|
||||||
|
LibretroFrontend::instance().setCoreOption(k, v);
|
||||||
|
env->ReleaseStringUTFChars(value, v);
|
||||||
|
env->ReleaseStringUTFChars(key, k);
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@@ -74,20 +74,14 @@ bool LibretroFrontend::loadCore(const char* soPath) {
|
|||||||
core_set_input_poll_(inputPollCallback);
|
core_set_input_poll_(inputPollCallback);
|
||||||
core_set_input_state_(inputStateCallback);
|
core_set_input_state_(inputStateCallback);
|
||||||
|
|
||||||
// Set controller to analog pad (DualShock) for PS1
|
|
||||||
core_set_controller_port_device_(0, RETRO_DEVICE_ANALOG);
|
|
||||||
|
|
||||||
retro_system_info sys_info{};
|
retro_system_info sys_info{};
|
||||||
core_get_system_info_(&sys_info);
|
core_get_system_info_(&sys_info);
|
||||||
|
need_fullpath_ = sys_info.need_fullpath;
|
||||||
LOGI("Core loaded: %s %s", sys_info.library_name, sys_info.library_version);
|
LOGI("Core loaded: %s %s", sys_info.library_name, sys_info.library_version);
|
||||||
LOGI(" need_fullpath: %d, block_extract: %d", sys_info.need_fullpath, sys_info.block_extract);
|
LOGI(" need_fullpath: %d, block_extract: %d", sys_info.need_fullpath, sys_info.block_extract);
|
||||||
|
|
||||||
// Set default core options for pcsx_rearmed
|
// Clear any previous core options
|
||||||
core_options_["pcsx_rearmed_drc"] = "enabled";
|
core_options_.clear();
|
||||||
core_options_["pcsx_rearmed_pad1type"] = "analog";
|
|
||||||
core_options_["pcsx_rearmed_frameskip_type"] = "disabled";
|
|
||||||
core_options_["pcsx_rearmed_region"] = "auto";
|
|
||||||
core_options_["pcsx_rearmed_memcard2"] = "disabled";
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -100,13 +94,29 @@ bool LibretroFrontend::loadGame(const char* romPath) {
|
|||||||
|
|
||||||
LOGI("Loading game: %s", romPath);
|
LOGI("Loading game: %s", romPath);
|
||||||
|
|
||||||
// pcsx_rearmed uses need_fullpath=true, so we just pass the path
|
|
||||||
retro_game_info game_info{};
|
retro_game_info game_info{};
|
||||||
game_info.path = romPath;
|
game_info.path = romPath;
|
||||||
game_info.data = nullptr;
|
|
||||||
game_info.size = 0;
|
|
||||||
game_info.meta = nullptr;
|
game_info.meta = nullptr;
|
||||||
|
|
||||||
|
if (need_fullpath_) {
|
||||||
|
game_info.data = nullptr;
|
||||||
|
game_info.size = 0;
|
||||||
|
} else {
|
||||||
|
// Core requires ROM data in memory
|
||||||
|
std::ifstream rom_file(romPath, std::ios::binary | std::ios::ate);
|
||||||
|
if (!rom_file) {
|
||||||
|
LOGE("Failed to open ROM file: %s", romPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t rom_size = rom_file.tellg();
|
||||||
|
rom_file.seekg(0);
|
||||||
|
rom_data_.resize(rom_size);
|
||||||
|
rom_file.read(reinterpret_cast<char*>(rom_data_.data()), rom_size);
|
||||||
|
game_info.data = rom_data_.data();
|
||||||
|
game_info.size = rom_size;
|
||||||
|
LOGI("ROM loaded into memory: %zu bytes", rom_size);
|
||||||
|
}
|
||||||
|
|
||||||
if (!core_load_game_(&game_info)) {
|
if (!core_load_game_(&game_info)) {
|
||||||
LOGE("retro_load_game failed");
|
LOGE("retro_load_game failed");
|
||||||
return false;
|
return false;
|
||||||
@@ -187,6 +197,8 @@ void LibretroFrontend::stop() {
|
|||||||
game_loaded_.store(false);
|
game_loaded_.store(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rom_data_.clear();
|
||||||
|
rom_data_.shrink_to_fit();
|
||||||
audio_engine_.stop();
|
audio_engine_.stop();
|
||||||
LOGI("Emulation stopped");
|
LOGI("Emulation stopped");
|
||||||
}
|
}
|
||||||
@@ -641,12 +653,6 @@ void LibretroFrontend::setDisplayMode(int mode) {
|
|||||||
|
|
||||||
void LibretroFrontend::setFrameSkip(int skip) {
|
void LibretroFrontend::setFrameSkip(int skip) {
|
||||||
frame_skip_ = skip;
|
frame_skip_ = skip;
|
||||||
if (skip > 0) {
|
|
||||||
core_options_["pcsx_rearmed_frameskip_type"] = "auto";
|
|
||||||
core_options_["pcsx_rearmed_frameskip_threshold"] = "33";
|
|
||||||
} else {
|
|
||||||
core_options_["pcsx_rearmed_frameskip_type"] = "disabled";
|
|
||||||
}
|
|
||||||
options_updated_.store(true);
|
options_updated_.store(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,3 +663,15 @@ void LibretroFrontend::setSystemDir(const char* dir) {
|
|||||||
void LibretroFrontend::setSaveDir(const char* dir) {
|
void LibretroFrontend::setSaveDir(const char* dir) {
|
||||||
save_dir_ = dir;
|
save_dir_ = dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LibretroFrontend::setControllerPortDevice(unsigned port, unsigned device) {
|
||||||
|
if (core_handle_ && core_set_controller_port_device_) {
|
||||||
|
core_set_controller_port_device_(port, device);
|
||||||
|
LOGI("Controller port %u set to device %u", port, device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LibretroFrontend::setCoreOption(const char* key, const char* value) {
|
||||||
|
core_options_[key] = value;
|
||||||
|
options_updated_.store(true);
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
enum class DisplayMode : int {
|
enum class DisplayMode : int {
|
||||||
STRETCH = 0,
|
STRETCH = 0,
|
||||||
@@ -43,6 +44,8 @@ public:
|
|||||||
|
|
||||||
void setSystemDir(const char* dir);
|
void setSystemDir(const char* dir);
|
||||||
void setSaveDir(const char* dir);
|
void setSaveDir(const char* dir);
|
||||||
|
void setControllerPortDevice(unsigned port, unsigned device);
|
||||||
|
void setCoreOption(const char* key, const char* value);
|
||||||
|
|
||||||
bool isRunning() const { return running_.load(); }
|
bool isRunning() const { return running_.load(); }
|
||||||
|
|
||||||
@@ -114,6 +117,10 @@ private:
|
|||||||
std::string system_dir_;
|
std::string system_dir_;
|
||||||
std::string save_dir_;
|
std::string save_dir_;
|
||||||
|
|
||||||
|
// ROM data (kept alive for cores with need_fullpath=false)
|
||||||
|
std::vector<uint8_t> rom_data_;
|
||||||
|
bool need_fullpath_ = true;
|
||||||
|
|
||||||
// Frame timing
|
// Frame timing
|
||||||
double target_fps_ = 60.0;
|
double target_fps_ = 60.0;
|
||||||
int frame_skip_ = 0;
|
int frame_skip_ = 0;
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ class CoverArtManager(private val context: Context) {
|
|||||||
|
|
||||||
val dest = cacheFile(gameTitle, consoleType)
|
val dest = cacheFile(gameTitle, consoleType)
|
||||||
|
|
||||||
// Try the title as-is first, then cleaned up
|
// Try the title as-is, with expanded regions, then fully cleaned
|
||||||
val namesToTry = buildList {
|
val namesToTry = buildList {
|
||||||
add(gameTitle)
|
add(gameTitle)
|
||||||
|
val expanded = expandRegions(gameTitle)
|
||||||
|
expanded.filter { it != gameTitle }.forEach { add(it) }
|
||||||
val cleaned = cleanTitle(gameTitle)
|
val cleaned = cleanTitle(gameTitle)
|
||||||
if (cleaned != gameTitle) add(cleaned)
|
if (cleaned != gameTitle && cleaned !in expanded) add(cleaned)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (name in namesToTry) {
|
for (name in namesToTry) {
|
||||||
@@ -87,6 +89,33 @@ class CoverArtManager(private val context: Context) {
|
|||||||
private const val TAG = "CoverArtManager"
|
private const val TAG = "CoverArtManager"
|
||||||
private const val BASE_URL = "https://raw.githubusercontent.com/libretro-thumbnails"
|
private const val BASE_URL = "https://raw.githubusercontent.com/libretro-thumbnails"
|
||||||
|
|
||||||
|
// GoodNES (U) can mean (USA) or (USA, Europe) — try both
|
||||||
|
private val regionExpansions = mapOf(
|
||||||
|
"(U)" to listOf("(USA, Europe)", "(USA)"),
|
||||||
|
"(E)" to listOf("(Europe)", "(Europe, Australia)"),
|
||||||
|
"(J)" to listOf("(Japan)", "(Japan, USA)"),
|
||||||
|
"(UE)" to listOf("(USA, Europe)"),
|
||||||
|
"(JU)" to listOf("(Japan, USA)"),
|
||||||
|
"(W)" to listOf("(World)"),
|
||||||
|
"(F)" to listOf("(France)"),
|
||||||
|
"(G)" to listOf("(Germany)"),
|
||||||
|
"(S)" to listOf("(Spain)"),
|
||||||
|
"(I)" to listOf("(Italy)"),
|
||||||
|
"(Unl)" to listOf("(USA) (Unl)", "(Unl)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun expandRegions(title: String): List<String> {
|
||||||
|
val stripped = title
|
||||||
|
.replace(Regex("\\s*\\[[^]]*]"), "") // strip flags like [!], [b], [p1]
|
||||||
|
.trim()
|
||||||
|
for ((short, expansions) in regionExpansions) {
|
||||||
|
if (short in stripped) {
|
||||||
|
return expansions.map { stripped.replace(short, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listOf(stripped)
|
||||||
|
}
|
||||||
|
|
||||||
fun cleanTitle(title: String): String {
|
fun cleanTitle(title: String): String {
|
||||||
return title
|
return title
|
||||||
// Remove region tags: (USA), (Europe), (Japan), etc.
|
// Remove region tags: (USA), (Europe), (Japan), etc.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ enum class ConsoleType(
|
|||||||
displayName = "Nintendo Entertainment System",
|
displayName = "Nintendo Entertainment System",
|
||||||
manufacturer = "Nintendo",
|
manufacturer = "Nintendo",
|
||||||
fileExtensions = listOf("nes", "unf", "unif"),
|
fileExtensions = listOf("nes", "unf", "unif"),
|
||||||
|
isImplemented = true,
|
||||||
fallbackAspectRatio = 256f / 240f,
|
fallbackAspectRatio = 256f / 240f,
|
||||||
libretroThumbnailRepo = "Nintendo_-_Nintendo_Entertainment_System"
|
libretroThumbnailRepo = "Nintendo_-_Nintendo_Entertainment_System"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.lazy.emulate.emulation
|
package com.lazy.emulate.emulation
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.lazy.emulate.emulation.cores.NesCore
|
||||||
import com.lazy.emulate.emulation.cores.PlaceholderCore
|
import com.lazy.emulate.emulation.cores.PlaceholderCore
|
||||||
import com.lazy.emulate.emulation.cores.Ps1Core
|
import com.lazy.emulate.emulation.cores.Ps1Core
|
||||||
import com.lazy.emulate.input.ButtonMappingManager
|
import com.lazy.emulate.input.ButtonMappingManager
|
||||||
@@ -13,7 +14,7 @@ object EmulationEngine {
|
|||||||
fun getCoreForConsole(context: Context, consoleType: ConsoleType): EmulatorCore {
|
fun getCoreForConsole(context: Context, consoleType: ConsoleType): EmulatorCore {
|
||||||
return when (consoleType) {
|
return when (consoleType) {
|
||||||
ConsoleType.PS1 -> Ps1Core(context, buttonMappingManager)
|
ConsoleType.PS1 -> Ps1Core(context, buttonMappingManager)
|
||||||
ConsoleType.NES -> PlaceholderCore(ConsoleType.NES)
|
ConsoleType.NES -> NesCore(context, buttonMappingManager)
|
||||||
ConsoleType.SNES -> PlaceholderCore(ConsoleType.SNES)
|
ConsoleType.SNES -> PlaceholderCore(ConsoleType.SNES)
|
||||||
ConsoleType.N64 -> PlaceholderCore(ConsoleType.N64)
|
ConsoleType.N64 -> PlaceholderCore(ConsoleType.N64)
|
||||||
ConsoleType.GENESIS -> PlaceholderCore(ConsoleType.GENESIS)
|
ConsoleType.GENESIS -> PlaceholderCore(ConsoleType.GENESIS)
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ object NativeLibretro {
|
|||||||
external fun nativeSetDisplayMode(mode: Int)
|
external fun nativeSetDisplayMode(mode: Int)
|
||||||
external fun nativeSetSystemDir(dir: String)
|
external fun nativeSetSystemDir(dir: String)
|
||||||
external fun nativeSetSaveDir(dir: String)
|
external fun nativeSetSaveDir(dir: String)
|
||||||
|
external fun nativeSetControllerPortDevice(port: Int, device: Int)
|
||||||
|
external fun nativeSetCoreOption(key: String, value: String)
|
||||||
}
|
}
|
||||||
|
|||||||
225
app/src/main/java/com/lazy/emulate/emulation/cores/NesCore.kt
Normal file
225
app/src/main/java/com/lazy/emulate/emulation/cores/NesCore.kt
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package com.lazy.emulate.emulation.cores
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Surface
|
||||||
|
import com.lazy.emulate.emulation.ConsoleType
|
||||||
|
import com.lazy.emulate.emulation.DisplayMode
|
||||||
|
import com.lazy.emulate.emulation.EmulatorCore
|
||||||
|
import com.lazy.emulate.emulation.NativeLibretro
|
||||||
|
import com.lazy.emulate.emulation.SaveSlotInfo
|
||||||
|
import com.lazy.emulate.input.ButtonMappingManager
|
||||||
|
import com.lazy.emulate.input.GamepadButton
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class NesCore(
|
||||||
|
private val context: Context,
|
||||||
|
private val buttonMappingManager: ButtonMappingManager? = null
|
||||||
|
) : EmulatorCore {
|
||||||
|
|
||||||
|
override val consoleType = ConsoleType.NES
|
||||||
|
|
||||||
|
override var isRunning: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var buttonState: Int = 0
|
||||||
|
|
||||||
|
private val coresDir = File(context.filesDir, "cores")
|
||||||
|
private val systemDir = File(context.filesDir, "system")
|
||||||
|
private val baseSavesDir = File(context.filesDir, "saves")
|
||||||
|
private lateinit var savesDir: File
|
||||||
|
|
||||||
|
override fun loadRom(path: String): Boolean {
|
||||||
|
coresDir.mkdirs()
|
||||||
|
systemDir.mkdirs()
|
||||||
|
|
||||||
|
val romName = File(path).nameWithoutExtension
|
||||||
|
savesDir = File(baseSavesDir, romName)
|
||||||
|
savesDir.mkdirs()
|
||||||
|
|
||||||
|
NativeLibretro.nativeSetSystemDir(systemDir.absolutePath)
|
||||||
|
NativeLibretro.nativeSetSaveDir(savesDir.absolutePath)
|
||||||
|
|
||||||
|
val corePath = findCorePath()
|
||||||
|
if (corePath == null) {
|
||||||
|
Log.e(TAG, "NES core .so not found. Place fceumm_libretro_android.so in ${coresDir.absolutePath}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NativeLibretro.nativeLoadCore(corePath)) {
|
||||||
|
Log.e(TAG, "Failed to load core: $corePath")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NES uses standard joypad (already set by native loadCore)
|
||||||
|
|
||||||
|
if (!NativeLibretro.nativeLoadGame(path)) {
|
||||||
|
Log.e(TAG, "Failed to load game: $path")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Game loaded successfully: $path")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
isRunning = true
|
||||||
|
NativeLibretro.nativeStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pause() {
|
||||||
|
isRunning = false
|
||||||
|
NativeLibretro.nativePause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resume() {
|
||||||
|
isRunning = true
|
||||||
|
NativeLibretro.nativeResume()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
isRunning = false
|
||||||
|
NativeLibretro.nativeStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reset() {
|
||||||
|
NativeLibretro.nativeReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setSurface(surface: Surface) {
|
||||||
|
NativeLibretro.nativeSetSurface(surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onButtonPressed(button: GamepadButton) {
|
||||||
|
val bit = mapButtonToRetroId(button)
|
||||||
|
if (bit >= 0) {
|
||||||
|
buttonState = buttonState or (1 shl bit)
|
||||||
|
NativeLibretro.nativeSetButtonState(buttonState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onButtonReleased(button: GamepadButton) {
|
||||||
|
val bit = mapButtonToRetroId(button)
|
||||||
|
if (bit >= 0) {
|
||||||
|
buttonState = buttonState and (1 shl bit).inv()
|
||||||
|
NativeLibretro.nativeSetButtonState(buttonState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnalogStick(x: Float, y: Float, stickIndex: Int) {
|
||||||
|
// NES has no analog sticks
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveState(slot: Int): Boolean {
|
||||||
|
val path = File(savesDir, "state_$slot.sav").absolutePath
|
||||||
|
return NativeLibretro.nativeSaveState(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadState(slot: Int): Boolean {
|
||||||
|
val path = File(savesDir, "state_$slot.sav").absolutePath
|
||||||
|
return NativeLibretro.nativeLoadState(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSaveSlotInfo(slot: Int): SaveSlotInfo {
|
||||||
|
val file = File(savesDir, "state_$slot.sav")
|
||||||
|
val screenshot = File(savesDir, "state_$slot.jpg")
|
||||||
|
return SaveSlotInfo(
|
||||||
|
slot = slot,
|
||||||
|
exists = file.exists(),
|
||||||
|
lastModified = if (file.exists()) file.lastModified() else 0L,
|
||||||
|
screenshotPath = if (screenshot.exists()) screenshot.absolutePath else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getScreenshotPath(slot: Int): String =
|
||||||
|
File(savesDir, "state_$slot.jpg").absolutePath
|
||||||
|
|
||||||
|
override fun setAudioEnabled(enabled: Boolean) {
|
||||||
|
NativeLibretro.nativeSetAudioEnabled(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setFrameSkip(skip: Int) {
|
||||||
|
NativeLibretro.nativeSetFrameSkip(skip)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDisplayMode(mode: DisplayMode) {
|
||||||
|
NativeLibretro.nativeSetDisplayMode(mode.nativeValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findCorePath(): String? {
|
||||||
|
val names = listOf(
|
||||||
|
"fceumm_libretro_android.so",
|
||||||
|
"fceumm_libretro.so",
|
||||||
|
"nestopia_libretro_android.so",
|
||||||
|
"nestopia_libretro.so",
|
||||||
|
"mesen_libretro_android.so",
|
||||||
|
"mesen_libretro.so",
|
||||||
|
)
|
||||||
|
// Check internal cores dir first
|
||||||
|
for (name in names) {
|
||||||
|
val file = File(coresDir, name)
|
||||||
|
if (file.exists()) return file.absolutePath
|
||||||
|
}
|
||||||
|
// Extract bundled core from assets
|
||||||
|
for (name in names) {
|
||||||
|
try {
|
||||||
|
context.assets.open("cores/$name").use { input ->
|
||||||
|
val dest = File(coresDir, name)
|
||||||
|
dest.outputStream().use { output -> input.copyTo(output) }
|
||||||
|
dest.setExecutable(true)
|
||||||
|
Log.i(TAG, "Extracted bundled core: $name")
|
||||||
|
return dest.absolutePath
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Asset doesn't exist, try next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: auto-import from /sdcard/Games/cores/
|
||||||
|
val sdcardCores = File("/sdcard/Games/cores")
|
||||||
|
if (sdcardCores.exists()) {
|
||||||
|
for (name in names) {
|
||||||
|
val sdFile = File(sdcardCores, name)
|
||||||
|
if (sdFile.exists()) {
|
||||||
|
val dest = File(coresDir, name)
|
||||||
|
try {
|
||||||
|
sdFile.copyTo(dest, overwrite = true)
|
||||||
|
dest.setExecutable(true)
|
||||||
|
Log.i(TAG, "Imported core from sdcard: $name")
|
||||||
|
return dest.absolutePath
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to import core: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapButtonToRetroId(button: GamepadButton): Int {
|
||||||
|
if (buttonMappingManager != null) {
|
||||||
|
return buttonMappingManager.getRetroId(consoleType, button)
|
||||||
|
}
|
||||||
|
return defaultMapButtonToRetroId(button)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultMapButtonToRetroId(button: GamepadButton): Int {
|
||||||
|
// NES: A, B, Select, Start, D-pad
|
||||||
|
// Libretro joypad IDs: B=0, Y=1, Select=2, Start=3,
|
||||||
|
// Up=4, Down=5, Left=6, Right=7, A=8, X=9
|
||||||
|
return when (button) {
|
||||||
|
GamepadButton.FACE_BOTTOM -> 0 // B
|
||||||
|
GamepadButton.FACE_RIGHT -> 8 // A
|
||||||
|
GamepadButton.DPAD_UP -> 4
|
||||||
|
GamepadButton.DPAD_DOWN -> 5
|
||||||
|
GamepadButton.DPAD_LEFT -> 6
|
||||||
|
GamepadButton.DPAD_RIGHT -> 7
|
||||||
|
GamepadButton.SELECT -> 2
|
||||||
|
GamepadButton.START -> 3
|
||||||
|
else -> -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "NesCore"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,11 +59,21 @@ class Ps1Core(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set core options before loading game
|
||||||
|
NativeLibretro.nativeSetCoreOption("pcsx_rearmed_drc", "enabled")
|
||||||
|
NativeLibretro.nativeSetCoreOption("pcsx_rearmed_pad1type", "analog")
|
||||||
|
NativeLibretro.nativeSetCoreOption("pcsx_rearmed_frameskip_type", "disabled")
|
||||||
|
NativeLibretro.nativeSetCoreOption("pcsx_rearmed_region", "auto")
|
||||||
|
NativeLibretro.nativeSetCoreOption("pcsx_rearmed_memcard2", "disabled")
|
||||||
|
|
||||||
if (!NativeLibretro.nativeLoadGame(path)) {
|
if (!NativeLibretro.nativeLoadGame(path)) {
|
||||||
Log.e(TAG, "Failed to load game: $path")
|
Log.e(TAG, "Failed to load game: $path")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set controller to DualShock analog after game load
|
||||||
|
NativeLibretro.nativeSetControllerPortDevice(0, 5) // RETRO_DEVICE_ANALOG
|
||||||
|
|
||||||
Log.i(TAG, "Game loaded successfully: $path")
|
Log.i(TAG, "Game loaded successfully: $path")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -172,8 +182,21 @@ class Ps1Core(
|
|||||||
val file = File(coresDir, name)
|
val file = File(coresDir, name)
|
||||||
if (file.exists()) return file.absolutePath
|
if (file.exists()) return file.absolutePath
|
||||||
}
|
}
|
||||||
// Auto-import from /sdcard/Games/cores/ into internal storage
|
// Extract bundled core from assets
|
||||||
// (dlopen requires the .so to be in app-writable storage, not sdcard)
|
for (name in names) {
|
||||||
|
try {
|
||||||
|
context.assets.open("cores/$name").use { input ->
|
||||||
|
val dest = File(coresDir, name)
|
||||||
|
dest.outputStream().use { output -> input.copyTo(output) }
|
||||||
|
dest.setExecutable(true)
|
||||||
|
Log.i(TAG, "Extracted bundled core: $name")
|
||||||
|
return dest.absolutePath
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Asset doesn't exist, try next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: auto-import from /sdcard/Games/cores/
|
||||||
val sdcardCores = File("/sdcard/Games/cores")
|
val sdcardCores = File("/sdcard/Games/cores")
|
||||||
if (sdcardCores.exists()) {
|
if (sdcardCores.exists()) {
|
||||||
for (name in names) {
|
for (name in names) {
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ class ButtonMappingManager(private val preferencesManager: PreferencesManager) {
|
|||||||
GamepadButton.R3 to 15,
|
GamepadButton.R3 to 15,
|
||||||
GamepadButton.SELECT to 2,
|
GamepadButton.SELECT to 2,
|
||||||
GamepadButton.START to 3
|
GamepadButton.START to 3
|
||||||
|
),
|
||||||
|
ConsoleType.NES to mapOf(
|
||||||
|
GamepadButton.FACE_BOTTOM to 0, // B
|
||||||
|
GamepadButton.FACE_RIGHT to 8, // A
|
||||||
|
GamepadButton.DPAD_UP to 4,
|
||||||
|
GamepadButton.DPAD_DOWN to 5,
|
||||||
|
GamepadButton.DPAD_LEFT to 6,
|
||||||
|
GamepadButton.DPAD_RIGHT to 7,
|
||||||
|
GamepadButton.SELECT to 2,
|
||||||
|
GamepadButton.START to 3
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user