Merge feature/snes: SNES emulation with bundled Snes9x core

This commit is contained in:
2026-04-10 20:03:02 -04:00
5 changed files with 252 additions and 1 deletions

Binary file not shown.

View File

@@ -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"
), ),

View File

@@ -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)

View 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"
}
}

View File

@@ -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
) )
) )