Add SNES emulation support with bundled Snes9x core
Bundles snes9x_libretro_android.so (~2.8 MB) as an asset and wires up SnesCore with full SNES button mappings (B/A/Y/X/L/R/Select/Start/D-pad).
This commit is contained in:
BIN
app/src/main/assets/cores/snes9x_libretro_android.so
Executable file
BIN
app/src/main/assets/cores/snes9x_libretro_android.so
Executable file
Binary file not shown.
@@ -28,6 +28,7 @@ enum class ConsoleType(
|
|||||||
displayName = "Super Nintendo",
|
displayName = "Super Nintendo",
|
||||||
manufacturer = "Nintendo",
|
manufacturer = "Nintendo",
|
||||||
fileExtensions = listOf("sfc", "smc", "fig", "swc"),
|
fileExtensions = listOf("sfc", "smc", "fig", "swc"),
|
||||||
|
isImplemented = true,
|
||||||
fallbackAspectRatio = 4f / 3f,
|
fallbackAspectRatio = 4f / 3f,
|
||||||
libretroThumbnailRepo = "Nintendo_-_Super_Nintendo_Entertainment_System"
|
libretroThumbnailRepo = "Nintendo_-_Super_Nintendo_Entertainment_System"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import com.lazy.emulate.emulation.cores.NesCore
|
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.emulation.cores.SnesCore
|
||||||
import com.lazy.emulate.input.ButtonMappingManager
|
import com.lazy.emulate.input.ButtonMappingManager
|
||||||
|
|
||||||
object EmulationEngine {
|
object EmulationEngine {
|
||||||
@@ -15,7 +16,7 @@ object EmulationEngine {
|
|||||||
return when (consoleType) {
|
return when (consoleType) {
|
||||||
ConsoleType.PS1 -> Ps1Core(context, buttonMappingManager)
|
ConsoleType.PS1 -> Ps1Core(context, buttonMappingManager)
|
||||||
ConsoleType.NES -> NesCore(context, buttonMappingManager)
|
ConsoleType.NES -> NesCore(context, buttonMappingManager)
|
||||||
ConsoleType.SNES -> PlaceholderCore(ConsoleType.SNES)
|
ConsoleType.SNES -> SnesCore(context, buttonMappingManager)
|
||||||
ConsoleType.N64 -> PlaceholderCore(ConsoleType.N64)
|
ConsoleType.N64 -> PlaceholderCore(ConsoleType.N64)
|
||||||
ConsoleType.GENESIS -> PlaceholderCore(ConsoleType.GENESIS)
|
ConsoleType.GENESIS -> PlaceholderCore(ConsoleType.GENESIS)
|
||||||
ConsoleType.PS2 -> PlaceholderCore(ConsoleType.PS2)
|
ConsoleType.PS2 -> PlaceholderCore(ConsoleType.PS2)
|
||||||
|
|||||||
235
app/src/main/java/com/lazy/emulate/emulation/cores/SnesCore.kt
Normal file
235
app/src/main/java/com/lazy/emulate/emulation/cores/SnesCore.kt
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,20 @@ class ButtonMappingManager(private val preferencesManager: PreferencesManager) {
|
|||||||
GamepadButton.DPAD_RIGHT to 7,
|
GamepadButton.DPAD_RIGHT to 7,
|
||||||
GamepadButton.SELECT to 2,
|
GamepadButton.SELECT to 2,
|
||||||
GamepadButton.START to 3
|
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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user