diff --git a/app/src/main/assets/cores/genesis_plus_gx_libretro_android.so b/app/src/main/assets/cores/genesis_plus_gx_libretro_android.so new file mode 100755 index 0000000..ca3868a Binary files /dev/null and b/app/src/main/assets/cores/genesis_plus_gx_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 3fdfcc0..ed697cb 100644 --- a/app/src/main/java/com/lazy/emulate/emulation/ConsoleType.kt +++ b/app/src/main/java/com/lazy/emulate/emulation/ConsoleType.kt @@ -43,6 +43,7 @@ enum class ConsoleType( displayName = "Sega Genesis", manufacturer = "Sega", fileExtensions = listOf("gen", "md", "smd", "bin"), + isImplemented = true, fallbackAspectRatio = 4f / 3f, libretroThumbnailRepo = "Sega_-_Mega_Drive_-_Genesis" ), 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 9eb366d..d605cc2 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.GenesisCore import com.lazy.emulate.emulation.cores.NesCore import com.lazy.emulate.emulation.cores.PlaceholderCore import com.lazy.emulate.emulation.cores.Ps1Core @@ -18,7 +19,7 @@ object EmulationEngine { ConsoleType.NES -> NesCore(context, buttonMappingManager) ConsoleType.SNES -> SnesCore(context, buttonMappingManager) ConsoleType.N64 -> PlaceholderCore(ConsoleType.N64) - ConsoleType.GENESIS -> PlaceholderCore(ConsoleType.GENESIS) + ConsoleType.GENESIS -> GenesisCore(context, buttonMappingManager) ConsoleType.PS2 -> PlaceholderCore(ConsoleType.PS2) } } diff --git a/app/src/main/java/com/lazy/emulate/emulation/cores/GenesisCore.kt b/app/src/main/java/com/lazy/emulate/emulation/cores/GenesisCore.kt new file mode 100644 index 0000000..f425758 --- /dev/null +++ b/app/src/main/java/com/lazy/emulate/emulation/cores/GenesisCore.kt @@ -0,0 +1,242 @@ +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 GenesisCore( + private val context: Context, + private val buttonMappingManager: ButtonMappingManager? = null +) : EmulatorCore { + + override val consoleType = ConsoleType.GENESIS + + 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, "Genesis core .so not found. Place genesis_plus_gx_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) { + // Genesis 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( + "genesis_plus_gx_libretro_android.so", + "genesis_plus_gx_libretro.so", + "picodrive_libretro_android.so", + "picodrive_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 { + // Genesis Plus GX default retropad -> Genesis mapping: + // RETRO_Y (1) -> Genesis A + // RETRO_B (0) -> Genesis B + // RETRO_A (8) -> Genesis C + // RETRO_L (10) -> Genesis X (6-button) + // RETRO_X (9) -> Genesis Y (6-button) + // RETRO_R (11) -> Genesis Z (6-button) + // RETRO_SELECT (2) -> Mode + // RETRO_START (3) -> Start + return when (button) { + GamepadButton.FACE_BOTTOM -> 1 // Genesis A + GamepadButton.FACE_RIGHT -> 0 // Genesis B + GamepadButton.FACE_LEFT -> 8 // Genesis C + GamepadButton.FACE_TOP -> 9 // Genesis Y (6-button) + GamepadButton.L1 -> 10 // Genesis X (6-button) + GamepadButton.R1 -> 11 // Genesis Z (6-button) + GamepadButton.C_BUTTON -> 8 // alt binding for C + GamepadButton.Z_BUTTON -> 11 // alt binding for Z + GamepadButton.DPAD_UP -> 4 + GamepadButton.DPAD_DOWN -> 5 + GamepadButton.DPAD_LEFT -> 6 + GamepadButton.DPAD_RIGHT -> 7 + GamepadButton.MODE -> 2 + GamepadButton.SELECT -> 2 + GamepadButton.START -> 3 + else -> -1 + } + } + + companion object { + private const val TAG = "GenesisCore" + } +} 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 76d0aeb..9c1b6af 100644 --- a/app/src/main/java/com/lazy/emulate/input/ButtonMappingManager.kt +++ b/app/src/main/java/com/lazy/emulate/input/ButtonMappingManager.kt @@ -47,6 +47,23 @@ class ButtonMappingManager(private val preferencesManager: PreferencesManager) { GamepadButton.DPAD_RIGHT to 7, GamepadButton.SELECT to 2, GamepadButton.START to 3 + ), + ConsoleType.GENESIS to mapOf( + // Genesis Plus GX default retropad -> Genesis mapping + GamepadButton.FACE_BOTTOM to 1, // Genesis A (RETRO_Y) + GamepadButton.FACE_RIGHT to 0, // Genesis B (RETRO_B) + GamepadButton.FACE_LEFT to 8, // Genesis C (RETRO_A) + GamepadButton.FACE_TOP to 9, // Genesis Y (6-button) + GamepadButton.L1 to 10, // Genesis X (6-button) + GamepadButton.R1 to 11, // Genesis Z (6-button) + GamepadButton.C_BUTTON to 8, // alt C + GamepadButton.Z_BUTTON to 11, // alt Z + GamepadButton.DPAD_UP to 4, + GamepadButton.DPAD_DOWN to 5, + GamepadButton.DPAD_LEFT to 6, + GamepadButton.DPAD_RIGHT to 7, + GamepadButton.MODE to 2, // Genesis Mode + GamepadButton.START to 3 ) )