Merge feature/snes: SNES emulation with bundled Snes9x core
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",
|
||||
manufacturer = "Nintendo",
|
||||
fileExtensions = listOf("sfc", "smc", "fig", "swc"),
|
||||
isImplemented = true,
|
||||
fallbackAspectRatio = 4f / 3f,
|
||||
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.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)
|
||||
|
||||
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.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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user