diff --git a/app/src/main/assets/cores/snes9x_libretro_android.so b/app/src/main/assets/cores/snes9x_libretro_android.so new file mode 100755 index 0000000..9d6a008 Binary files /dev/null and b/app/src/main/assets/cores/snes9x_libretro_android.so differ 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 83897ff..3fdfcc0 100644 --- a/app/src/main/java/com/lazy/emulate/emulation/ConsoleType.kt +++ b/app/src/main/java/com/lazy/emulate/emulation/ConsoleType.kt @@ -28,6 +28,7 @@ enum class ConsoleType( displayName = "Super Nintendo", manufacturer = "Nintendo", fileExtensions = listOf("sfc", "smc", "fig", "swc"), + isImplemented = true, fallbackAspectRatio = 4f / 3f, libretroThumbnailRepo = "Nintendo_-_Super_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 607ef9a..9eb366d 100644 --- a/app/src/main/java/com/lazy/emulate/emulation/EmulationEngine.kt +++ b/app/src/main/java/com/lazy/emulate/emulation/EmulationEngine.kt @@ -4,6 +4,7 @@ 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.emulation.cores.SnesCore import com.lazy.emulate.input.ButtonMappingManager object EmulationEngine { @@ -15,7 +16,7 @@ object EmulationEngine { return when (consoleType) { ConsoleType.PS1 -> Ps1Core(context, buttonMappingManager) ConsoleType.NES -> NesCore(context, buttonMappingManager) - ConsoleType.SNES -> PlaceholderCore(ConsoleType.SNES) + ConsoleType.SNES -> SnesCore(context, buttonMappingManager) ConsoleType.N64 -> PlaceholderCore(ConsoleType.N64) ConsoleType.GENESIS -> PlaceholderCore(ConsoleType.GENESIS) ConsoleType.PS2 -> PlaceholderCore(ConsoleType.PS2) diff --git a/app/src/main/java/com/lazy/emulate/emulation/cores/SnesCore.kt b/app/src/main/java/com/lazy/emulate/emulation/cores/SnesCore.kt new file mode 100644 index 0000000..693d221 --- /dev/null +++ b/app/src/main/java/com/lazy/emulate/emulation/cores/SnesCore.kt @@ -0,0 +1,235 @@ +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 SnesCore( + private val context: Context, + private val buttonMappingManager: ButtonMappingManager? = null +) : EmulatorCore { + + override val consoleType = ConsoleType.SNES + + 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, "SNES core .so not found. Place snes9x_libretro_android.so in ${coresDir.absolutePath}") + return false + } + + if (!NativeLibretro.nativeLoadCore(corePath)) { + Log.e(TAG, "Failed to load core: $corePath") + return false + } + + 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) { + // SNES 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 deleteState(slot: Int): Boolean { + val state = File(savesDir, "state_$slot.sav") + val screenshot = File(savesDir, "state_$slot.jpg") + val deleted = state.exists() && state.delete() + if (screenshot.exists()) screenshot.delete() + return deleted + } + + 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( + "snes9x_libretro_android.so", + "snes9x_libretro.so", + "snes9x2010_libretro_android.so", + "snes9x2010_libretro.so", + "snes9x2005_libretro_android.so", + "snes9x2005_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 { + // SNES: B, A, Y, X, L, R, 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, L=10, R=11 + return when (button) { + GamepadButton.FACE_BOTTOM -> 0 // B + GamepadButton.FACE_RIGHT -> 8 // A + GamepadButton.FACE_LEFT -> 1 // Y + GamepadButton.FACE_TOP -> 9 // X + GamepadButton.L1 -> 10 // L + GamepadButton.R1 -> 11 // R + 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 = "SnesCore" + } +} 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 2256c6f..76d0aeb 100644 --- a/app/src/main/java/com/lazy/emulate/input/ButtonMappingManager.kt +++ b/app/src/main/java/com/lazy/emulate/input/ButtonMappingManager.kt @@ -33,6 +33,20 @@ class ButtonMappingManager(private val preferencesManager: PreferencesManager) { GamepadButton.DPAD_RIGHT to 7, GamepadButton.SELECT to 2, GamepadButton.START to 3 + ), + ConsoleType.SNES to mapOf( + GamepadButton.FACE_BOTTOM to 0, // B + GamepadButton.FACE_RIGHT to 8, // A + GamepadButton.FACE_LEFT to 1, // Y + GamepadButton.FACE_TOP to 9, // X + GamepadButton.L1 to 10, // L + GamepadButton.R1 to 11, // R + 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 ) )