diff --git a/app/src/main/assets/cores/fceumm_libretro_android.so b/app/src/main/assets/cores/fceumm_libretro_android.so new file mode 100755 index 0000000..d239ca9 Binary files /dev/null and b/app/src/main/assets/cores/fceumm_libretro_android.so differ diff --git a/app/src/main/assets/cores/pcsx_rearmed_libretro_android.so b/app/src/main/assets/cores/pcsx_rearmed_libretro_android.so new file mode 100755 index 0000000..1fa3578 Binary files /dev/null and b/app/src/main/assets/cores/pcsx_rearmed_libretro_android.so differ diff --git a/app/src/main/cpp/jni_bridge.cpp b/app/src/main/cpp/jni_bridge.cpp index b9c1954..579eb4b 100644 --- a/app/src/main/cpp/jni_bridge.cpp +++ b/app/src/main/cpp/jni_bridge.cpp @@ -121,4 +121,18 @@ Java_com_lazy_emulate_emulation_NativeLibretro_nativeSetSaveDir(JNIEnv* env, job 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(port), static_cast(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" diff --git a/app/src/main/cpp/libretro_frontend.cpp b/app/src/main/cpp/libretro_frontend.cpp index 761c5af..463aae3 100644 --- a/app/src/main/cpp/libretro_frontend.cpp +++ b/app/src/main/cpp/libretro_frontend.cpp @@ -74,20 +74,14 @@ bool LibretroFrontend::loadCore(const char* soPath) { core_set_input_poll_(inputPollCallback); 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{}; 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(" need_fullpath: %d, block_extract: %d", sys_info.need_fullpath, sys_info.block_extract); - // Set default core options for pcsx_rearmed - core_options_["pcsx_rearmed_drc"] = "enabled"; - 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"; + // Clear any previous core options + core_options_.clear(); return true; } @@ -100,13 +94,29 @@ bool LibretroFrontend::loadGame(const char* romPath) { LOGI("Loading game: %s", romPath); - // pcsx_rearmed uses need_fullpath=true, so we just pass the path retro_game_info game_info{}; game_info.path = romPath; - game_info.data = nullptr; - game_info.size = 0; 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(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)) { LOGE("retro_load_game failed"); return false; @@ -187,6 +197,8 @@ void LibretroFrontend::stop() { game_loaded_.store(false); } + rom_data_.clear(); + rom_data_.shrink_to_fit(); audio_engine_.stop(); LOGI("Emulation stopped"); } @@ -641,12 +653,6 @@ void LibretroFrontend::setDisplayMode(int mode) { void LibretroFrontend::setFrameSkip(int 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); } @@ -657,3 +663,15 @@ void LibretroFrontend::setSystemDir(const char* dir) { void LibretroFrontend::setSaveDir(const char* 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); +} diff --git a/app/src/main/cpp/libretro_frontend.h b/app/src/main/cpp/libretro_frontend.h index 0f08599..832202b 100644 --- a/app/src/main/cpp/libretro_frontend.h +++ b/app/src/main/cpp/libretro_frontend.h @@ -9,6 +9,7 @@ #include #include #include +#include enum class DisplayMode : int { STRETCH = 0, @@ -43,6 +44,8 @@ public: void setSystemDir(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(); } @@ -114,6 +117,10 @@ private: std::string system_dir_; std::string save_dir_; + // ROM data (kept alive for cores with need_fullpath=false) + std::vector rom_data_; + bool need_fullpath_ = true; + // Frame timing double target_fps_ = 60.0; int frame_skip_ = 0; diff --git a/app/src/main/java/com/lazy/emulate/data/CoverArtManager.kt b/app/src/main/java/com/lazy/emulate/data/CoverArtManager.kt index 6b0daee..b68f12b 100644 --- a/app/src/main/java/com/lazy/emulate/data/CoverArtManager.kt +++ b/app/src/main/java/com/lazy/emulate/data/CoverArtManager.kt @@ -31,11 +31,13 @@ class CoverArtManager(private val context: Context) { 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 { add(gameTitle) + val expanded = expandRegions(gameTitle) + expanded.filter { it != gameTitle }.forEach { add(it) } val cleaned = cleanTitle(gameTitle) - if (cleaned != gameTitle) add(cleaned) + if (cleaned != gameTitle && cleaned !in expanded) add(cleaned) } for (name in namesToTry) { @@ -87,6 +89,33 @@ class CoverArtManager(private val context: Context) { private const val TAG = "CoverArtManager" 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 { + 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 { return title // Remove region tags: (USA), (Europe), (Japan), etc. diff --git a/app/src/main/java/com/lazy/emulate/emulation/ConsoleType.kt b/app/src/main/java/com/lazy/emulate/emulation/ConsoleType.kt index 49c6f1e..83897ff 100644 --- a/app/src/main/java/com/lazy/emulate/emulation/ConsoleType.kt +++ b/app/src/main/java/com/lazy/emulate/emulation/ConsoleType.kt @@ -20,6 +20,7 @@ enum class ConsoleType( displayName = "Nintendo Entertainment System", manufacturer = "Nintendo", fileExtensions = listOf("nes", "unf", "unif"), + isImplemented = true, fallbackAspectRatio = 256f / 240f, libretroThumbnailRepo = "Nintendo_-_Nintendo_Entertainment_System" ), diff --git a/app/src/main/java/com/lazy/emulate/emulation/EmulationEngine.kt b/app/src/main/java/com/lazy/emulate/emulation/EmulationEngine.kt index a619598..607ef9a 100644 --- a/app/src/main/java/com/lazy/emulate/emulation/EmulationEngine.kt +++ b/app/src/main/java/com/lazy/emulate/emulation/EmulationEngine.kt @@ -1,6 +1,7 @@ package com.lazy.emulate.emulation import android.content.Context +import com.lazy.emulate.emulation.cores.NesCore import com.lazy.emulate.emulation.cores.PlaceholderCore import com.lazy.emulate.emulation.cores.Ps1Core import com.lazy.emulate.input.ButtonMappingManager @@ -13,7 +14,7 @@ object EmulationEngine { fun getCoreForConsole(context: Context, consoleType: ConsoleType): EmulatorCore { return when (consoleType) { ConsoleType.PS1 -> Ps1Core(context, buttonMappingManager) - ConsoleType.NES -> PlaceholderCore(ConsoleType.NES) + ConsoleType.NES -> NesCore(context, buttonMappingManager) ConsoleType.SNES -> PlaceholderCore(ConsoleType.SNES) ConsoleType.N64 -> PlaceholderCore(ConsoleType.N64) ConsoleType.GENESIS -> PlaceholderCore(ConsoleType.GENESIS) diff --git a/app/src/main/java/com/lazy/emulate/emulation/NativeLibretro.kt b/app/src/main/java/com/lazy/emulate/emulation/NativeLibretro.kt index d8b94a6..b6c76e5 100644 --- a/app/src/main/java/com/lazy/emulate/emulation/NativeLibretro.kt +++ b/app/src/main/java/com/lazy/emulate/emulation/NativeLibretro.kt @@ -26,4 +26,6 @@ object NativeLibretro { external fun nativeSetDisplayMode(mode: Int) external fun nativeSetSystemDir(dir: String) external fun nativeSetSaveDir(dir: String) + external fun nativeSetControllerPortDevice(port: Int, device: Int) + external fun nativeSetCoreOption(key: String, value: String) } diff --git a/app/src/main/java/com/lazy/emulate/emulation/cores/NesCore.kt b/app/src/main/java/com/lazy/emulate/emulation/cores/NesCore.kt new file mode 100644 index 0000000..d6b7a8d --- /dev/null +++ b/app/src/main/java/com/lazy/emulate/emulation/cores/NesCore.kt @@ -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" + } +} diff --git a/app/src/main/java/com/lazy/emulate/emulation/cores/Ps1Core.kt b/app/src/main/java/com/lazy/emulate/emulation/cores/Ps1Core.kt index 8880180..1fa2055 100644 --- a/app/src/main/java/com/lazy/emulate/emulation/cores/Ps1Core.kt +++ b/app/src/main/java/com/lazy/emulate/emulation/cores/Ps1Core.kt @@ -59,11 +59,21 @@ class Ps1Core( 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)) { Log.e(TAG, "Failed to load game: $path") return false } + // Set controller to DualShock analog after game load + NativeLibretro.nativeSetControllerPortDevice(0, 5) // RETRO_DEVICE_ANALOG + Log.i(TAG, "Game loaded successfully: $path") return true } @@ -172,8 +182,21 @@ class Ps1Core( val file = File(coresDir, name) if (file.exists()) return file.absolutePath } - // Auto-import from /sdcard/Games/cores/ into internal storage - // (dlopen requires the .so to be in app-writable storage, not sdcard) + // 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) { diff --git a/app/src/main/java/com/lazy/emulate/input/ButtonMappingManager.kt b/app/src/main/java/com/lazy/emulate/input/ButtonMappingManager.kt index b7feed6..2256c6f 100644 --- a/app/src/main/java/com/lazy/emulate/input/ButtonMappingManager.kt +++ b/app/src/main/java/com/lazy/emulate/input/ButtonMappingManager.kt @@ -23,6 +23,16 @@ class ButtonMappingManager(private val preferencesManager: PreferencesManager) { GamepadButton.R3 to 15, GamepadButton.SELECT to 2, 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 ) )