mirror of
https://github.com/izzy2lost/xemu.git
synced 2026-07-06 00:20:22 -07:00
added EEPROM editor in settings change language and video standard
This commit is contained in:
@@ -4,6 +4,8 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -11,16 +13,58 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import java.io.File
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
private val prefs by lazy { getSharedPreferences("x1box_prefs", Context.MODE_PRIVATE) }
|
||||
|
||||
private data class EepromLanguageOption(
|
||||
val value: XboxEepromEditor.Language,
|
||||
val labelRes: Int,
|
||||
)
|
||||
|
||||
private data class EepromVideoOption(
|
||||
val value: XboxEepromEditor.VideoStandard,
|
||||
val labelRes: Int,
|
||||
)
|
||||
|
||||
private val eepromLanguageOptions = listOf(
|
||||
EepromLanguageOption(XboxEepromEditor.Language.ENGLISH, R.string.settings_eeprom_language_english),
|
||||
EepromLanguageOption(XboxEepromEditor.Language.JAPANESE, R.string.settings_eeprom_language_japanese),
|
||||
EepromLanguageOption(XboxEepromEditor.Language.GERMAN, R.string.settings_eeprom_language_german),
|
||||
EepromLanguageOption(XboxEepromEditor.Language.FRENCH, R.string.settings_eeprom_language_french),
|
||||
EepromLanguageOption(XboxEepromEditor.Language.SPANISH, R.string.settings_eeprom_language_spanish),
|
||||
EepromLanguageOption(XboxEepromEditor.Language.ITALIAN, R.string.settings_eeprom_language_italian),
|
||||
EepromLanguageOption(XboxEepromEditor.Language.KOREAN, R.string.settings_eeprom_language_korean),
|
||||
EepromLanguageOption(XboxEepromEditor.Language.CHINESE, R.string.settings_eeprom_language_chinese),
|
||||
EepromLanguageOption(XboxEepromEditor.Language.PORTUGUESE, R.string.settings_eeprom_language_portuguese),
|
||||
)
|
||||
|
||||
private val eepromVideoOptions = listOf(
|
||||
EepromVideoOption(XboxEepromEditor.VideoStandard.NTSC_M, R.string.settings_eeprom_video_standard_ntsc_m),
|
||||
EepromVideoOption(XboxEepromEditor.VideoStandard.NTSC_J, R.string.settings_eeprom_video_standard_ntsc_j),
|
||||
EepromVideoOption(XboxEepromEditor.VideoStandard.PAL_I, R.string.settings_eeprom_video_standard_pal_i),
|
||||
EepromVideoOption(XboxEepromEditor.VideoStandard.PAL_M, R.string.settings_eeprom_video_standard_pal_m),
|
||||
)
|
||||
|
||||
private var pendingVulkanUri: String? = null
|
||||
private var pendingVulkanName: String? = null
|
||||
private var clearVulkan = false
|
||||
|
||||
private lateinit var tvVulkanDriverName: TextView
|
||||
private lateinit var tvEepromStatus: TextView
|
||||
private lateinit var inputEepromLanguage: TextInputLayout
|
||||
private lateinit var inputEepromVideoStandard: TextInputLayout
|
||||
private lateinit var dropdownEepromLanguage: AutoCompleteTextView
|
||||
private lateinit var dropdownEepromVideoStandard: AutoCompleteTextView
|
||||
|
||||
private var selectedEepromLanguage = XboxEepromEditor.Language.ENGLISH
|
||||
private var selectedEepromVideoStandard = XboxEepromEditor.VideoStandard.NTSC_M
|
||||
private var eepromEditable = false
|
||||
private var eepromMissing = false
|
||||
private var eepromError = false
|
||||
|
||||
private val pickDriver =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
|
||||
@@ -55,6 +99,11 @@ class SettingsActivity : AppCompatActivity() {
|
||||
tvVulkanDriverName = findViewById(R.id.tv_vulkan_driver_name)
|
||||
val btnVulkanBrowse = findViewById<MaterialButton>(R.id.btn_vulkan_browse)
|
||||
val btnVulkanClear = findViewById<MaterialButton>(R.id.btn_vulkan_clear)
|
||||
tvEepromStatus = findViewById(R.id.tv_eeprom_status)
|
||||
inputEepromLanguage = findViewById(R.id.input_eeprom_language)
|
||||
inputEepromVideoStandard = findViewById(R.id.input_eeprom_video_standard)
|
||||
dropdownEepromLanguage = findViewById(R.id.dropdown_eeprom_language)
|
||||
dropdownEepromVideoStandard = findViewById(R.id.dropdown_eeprom_video_standard)
|
||||
|
||||
// Load current values
|
||||
val scale = prefs.getInt("setting_surface_scale", 1)
|
||||
@@ -119,6 +168,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||
tvVulkanDriverName.text = getString(R.string.settings_vulkan_driver_none)
|
||||
}
|
||||
|
||||
setupEepromEditor()
|
||||
|
||||
btnSave.setOnClickListener {
|
||||
val selectedDisplayMode = when (toggleDisplayMode.checkedButtonId) {
|
||||
R.id.btn_display_4_3 -> 1
|
||||
@@ -173,11 +224,137 @@ class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
edit.apply()
|
||||
|
||||
Toast.makeText(this, R.string.settings_saved, Toast.LENGTH_SHORT).show()
|
||||
val toastResult = applyEepromEdits()
|
||||
Toast.makeText(this, toastResult.first, toastResult.second).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupEepromEditor() {
|
||||
val languageLabels = eepromLanguageOptions.map { getString(it.labelRes) }
|
||||
val videoLabels = eepromVideoOptions.map { getString(it.labelRes) }
|
||||
|
||||
dropdownEepromLanguage.setAdapter(
|
||||
ArrayAdapter(this, android.R.layout.simple_list_item_1, languageLabels)
|
||||
)
|
||||
dropdownEepromVideoStandard.setAdapter(
|
||||
ArrayAdapter(this, android.R.layout.simple_list_item_1, videoLabels)
|
||||
)
|
||||
|
||||
dropdownEepromLanguage.setOnItemClickListener { _, _, position, _ ->
|
||||
selectedEepromLanguage = eepromLanguageOptions[position].value
|
||||
}
|
||||
dropdownEepromVideoStandard.setOnItemClickListener { _, _, position, _ ->
|
||||
selectedEepromVideoStandard = eepromVideoOptions[position].value
|
||||
}
|
||||
|
||||
val eepromFile = resolveEepromFile()
|
||||
if (!eepromFile.isFile) {
|
||||
eepromEditable = false
|
||||
eepromMissing = true
|
||||
eepromError = false
|
||||
setEepromEditorEnabled(false)
|
||||
setEepromLanguageSelection(selectedEepromLanguage)
|
||||
setEepromVideoSelection(selectedEepromVideoStandard)
|
||||
tvEepromStatus.text = getString(
|
||||
R.string.settings_eeprom_status_missing,
|
||||
eepromFile.absolutePath,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val snapshot = XboxEepromEditor.load(eepromFile)
|
||||
eepromEditable = true
|
||||
eepromMissing = false
|
||||
eepromError = false
|
||||
setEepromEditorEnabled(true)
|
||||
setEepromLanguageSelection(snapshot.language)
|
||||
setEepromVideoSelection(snapshot.videoStandard)
|
||||
|
||||
val hasUnknownValues =
|
||||
snapshot.rawLanguage != snapshot.language.id ||
|
||||
snapshot.rawVideoStandard != snapshot.videoStandard.id
|
||||
tvEepromStatus.text = if (hasUnknownValues) {
|
||||
getString(R.string.settings_eeprom_status_unknown, eepromFile.absolutePath)
|
||||
} else {
|
||||
getString(R.string.settings_eeprom_status_ready, eepromFile.absolutePath)
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
eepromEditable = false
|
||||
eepromMissing = false
|
||||
eepromError = true
|
||||
setEepromEditorEnabled(false)
|
||||
setEepromLanguageSelection(selectedEepromLanguage)
|
||||
setEepromVideoSelection(selectedEepromVideoStandard)
|
||||
tvEepromStatus.text = getString(
|
||||
R.string.settings_eeprom_status_invalid,
|
||||
eepromFile.absolutePath,
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
eepromEditable = false
|
||||
eepromMissing = false
|
||||
eepromError = true
|
||||
setEepromEditorEnabled(false)
|
||||
setEepromLanguageSelection(selectedEepromLanguage)
|
||||
setEepromVideoSelection(selectedEepromVideoStandard)
|
||||
tvEepromStatus.text = getString(
|
||||
R.string.settings_eeprom_status_error,
|
||||
eepromFile.absolutePath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyEepromEdits(): Pair<Int, Int> {
|
||||
if (eepromMissing) {
|
||||
return Pair(R.string.settings_saved_eeprom_missing, Toast.LENGTH_LONG)
|
||||
}
|
||||
if (eepromError || !eepromEditable) {
|
||||
return Pair(R.string.settings_saved_eeprom_failed, Toast.LENGTH_LONG)
|
||||
}
|
||||
|
||||
return try {
|
||||
val changed = XboxEepromEditor.apply(
|
||||
resolveEepromFile(),
|
||||
selectedEepromLanguage,
|
||||
selectedEepromVideoStandard,
|
||||
)
|
||||
if (changed) {
|
||||
Pair(R.string.settings_saved_with_eeprom, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
Pair(R.string.settings_saved, Toast.LENGTH_SHORT)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
Pair(R.string.settings_saved_eeprom_failed, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEepromEditorEnabled(enabled: Boolean) {
|
||||
inputEepromLanguage.isEnabled = enabled
|
||||
inputEepromVideoStandard.isEnabled = enabled
|
||||
dropdownEepromLanguage.isEnabled = enabled
|
||||
dropdownEepromVideoStandard.isEnabled = enabled
|
||||
}
|
||||
|
||||
private fun setEepromLanguageSelection(language: XboxEepromEditor.Language) {
|
||||
selectedEepromLanguage = language
|
||||
val option = eepromLanguageOptions.firstOrNull { it.value == language }
|
||||
?: eepromLanguageOptions.first()
|
||||
dropdownEepromLanguage.setText(getString(option.labelRes), false)
|
||||
}
|
||||
|
||||
private fun setEepromVideoSelection(video: XboxEepromEditor.VideoStandard) {
|
||||
selectedEepromVideoStandard = video
|
||||
val option = eepromVideoOptions.firstOrNull { it.value == video }
|
||||
?: eepromVideoOptions.first()
|
||||
dropdownEepromVideoStandard.setText(getString(option.labelRes), false)
|
||||
}
|
||||
|
||||
private fun resolveEepromFile(): File {
|
||||
val base = getExternalFilesDir(null) ?: filesDir
|
||||
return File(File(base, "x1box"), "eeprom.bin")
|
||||
}
|
||||
|
||||
private fun getFileName(uri: Uri): String? {
|
||||
return contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val col = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.izzy2lost.x1box
|
||||
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
internal object XboxEepromEditor {
|
||||
private const val EEPROM_SIZE = 256
|
||||
|
||||
private const val FACTORY_CHECKSUM_OFFSET = 0x30
|
||||
private const val FACTORY_CHECKSUM_START = 0x34
|
||||
private const val FACTORY_CHECKSUM_LENGTH = 0x2C
|
||||
|
||||
private const val USER_CHECKSUM_OFFSET = 0x60
|
||||
private const val USER_CHECKSUM_START = 0x64
|
||||
private const val USER_CHECKSUM_LENGTH = 0x5C
|
||||
|
||||
private const val VIDEO_STANDARD_OFFSET = 0x58
|
||||
private const val LANGUAGE_OFFSET = 0x90
|
||||
|
||||
enum class Language(val id: UInt) {
|
||||
ENGLISH(0x00000001u),
|
||||
JAPANESE(0x00000002u),
|
||||
GERMAN(0x00000003u),
|
||||
FRENCH(0x00000004u),
|
||||
SPANISH(0x00000005u),
|
||||
ITALIAN(0x00000006u),
|
||||
KOREAN(0x00000007u),
|
||||
CHINESE(0x00000008u),
|
||||
PORTUGUESE(0x00000009u);
|
||||
|
||||
companion object {
|
||||
fun fromId(id: UInt): Language? = entries.firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
enum class VideoStandard(val id: UInt) {
|
||||
NTSC_M(0x00400100u),
|
||||
NTSC_J(0x00400200u),
|
||||
PAL_I(0x00800300u),
|
||||
PAL_M(0x00400400u);
|
||||
|
||||
companion object {
|
||||
fun fromId(id: UInt): VideoStandard? = entries.firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
|
||||
data class Snapshot(
|
||||
val language: Language,
|
||||
val videoStandard: VideoStandard,
|
||||
val rawLanguage: UInt,
|
||||
val rawVideoStandard: UInt,
|
||||
)
|
||||
|
||||
@Throws(IOException::class, IllegalArgumentException::class)
|
||||
fun load(file: File): Snapshot {
|
||||
val data = file.readBytes()
|
||||
ensureValidSize(file, data)
|
||||
|
||||
val rawLanguage = readLeUInt32(data, LANGUAGE_OFFSET)
|
||||
val rawVideoStandard = readLeUInt32(data, VIDEO_STANDARD_OFFSET)
|
||||
|
||||
return Snapshot(
|
||||
language = Language.fromId(rawLanguage) ?: Language.ENGLISH,
|
||||
videoStandard = VideoStandard.fromId(rawVideoStandard) ?: VideoStandard.NTSC_M,
|
||||
rawLanguage = rawLanguage,
|
||||
rawVideoStandard = rawVideoStandard,
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, IllegalArgumentException::class)
|
||||
fun apply(file: File, language: Language, videoStandard: VideoStandard): Boolean {
|
||||
val data = file.readBytes()
|
||||
ensureValidSize(file, data)
|
||||
|
||||
val currentLanguage = readLeUInt32(data, LANGUAGE_OFFSET)
|
||||
val currentVideoStandard = readLeUInt32(data, VIDEO_STANDARD_OFFSET)
|
||||
if (currentLanguage == language.id && currentVideoStandard == videoStandard.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
writeLeUInt32(data, LANGUAGE_OFFSET, language.id)
|
||||
writeLeUInt32(data, VIDEO_STANDARD_OFFSET, videoStandard.id)
|
||||
|
||||
val factoryChecksum = xboxChecksum(data, FACTORY_CHECKSUM_START, FACTORY_CHECKSUM_LENGTH)
|
||||
writeLeUInt32(data, FACTORY_CHECKSUM_OFFSET, factoryChecksum)
|
||||
|
||||
val userChecksum = xboxChecksum(data, USER_CHECKSUM_START, USER_CHECKSUM_LENGTH)
|
||||
writeLeUInt32(data, USER_CHECKSUM_OFFSET, userChecksum)
|
||||
|
||||
file.writeBytes(data)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun ensureValidSize(file: File, data: ByteArray) {
|
||||
if (data.size != EEPROM_SIZE) {
|
||||
throw IllegalArgumentException(
|
||||
"Invalid EEPROM size ${data.size}, expected $EEPROM_SIZE (${file.absolutePath})"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readLeUInt32(data: ByteArray, offset: Int): UInt {
|
||||
return ((data[offset].toUInt() and 0xFFu) or
|
||||
((data[offset + 1].toUInt() and 0xFFu) shl 8) or
|
||||
((data[offset + 2].toUInt() and 0xFFu) shl 16) or
|
||||
((data[offset + 3].toUInt() and 0xFFu) shl 24))
|
||||
}
|
||||
|
||||
private fun writeLeUInt32(data: ByteArray, offset: Int, value: UInt) {
|
||||
data[offset] = (value and 0xFFu).toByte()
|
||||
data[offset + 1] = ((value shr 8) and 0xFFu).toByte()
|
||||
data[offset + 2] = ((value shr 16) and 0xFFu).toByte()
|
||||
data[offset + 3] = ((value shr 24) and 0xFFu).toByte()
|
||||
}
|
||||
|
||||
private fun xboxChecksum(data: ByteArray, offset: Int, length: Int): UInt {
|
||||
require(length % 4 == 0) { "Checksum length must be 32-bit aligned" }
|
||||
|
||||
var high = 0u
|
||||
var low = 0u
|
||||
var pos = offset
|
||||
val end = offset + length
|
||||
|
||||
while (pos < end) {
|
||||
val value = readLeUInt32(data, pos)
|
||||
val sum = (high.toULong() shl 32) or low.toULong()
|
||||
val next = sum + value.toULong()
|
||||
|
||||
high = ((next shr 32) and 0xFFFF_FFFFuL).toUInt()
|
||||
low += value
|
||||
pos += 4
|
||||
}
|
||||
|
||||
return (high + low).inv()
|
||||
}
|
||||
}
|
||||
@@ -405,6 +405,62 @@
|
||||
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/settings_section_eeprom"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="@color/xemu_green" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_eeprom_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="@color/xemu_text_muted" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_eeprom_language"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:hint="@string/settings_eeprom_language">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_eeprom_language"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_eeprom_video_standard"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/settings_eeprom_video_standard">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_eeprom_video_standard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/settings_eeprom_hint"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="@color/xemu_text_muted" />
|
||||
|
||||
<!-- Save button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_settings_save"
|
||||
|
||||
@@ -116,8 +116,33 @@
|
||||
<string name="settings_audio_driver_opensles">OpenSL ES</string>
|
||||
<string name="settings_audio_driver_aaudio">AAudio</string>
|
||||
<string name="settings_audio_driver_disabled">Disabled</string>
|
||||
<string name="settings_section_eeprom">EEPROM</string>
|
||||
<string name="settings_eeprom_language">EEPROM Language</string>
|
||||
<string name="settings_eeprom_language_english">English</string>
|
||||
<string name="settings_eeprom_language_japanese">Japanese</string>
|
||||
<string name="settings_eeprom_language_german">German</string>
|
||||
<string name="settings_eeprom_language_french">French</string>
|
||||
<string name="settings_eeprom_language_spanish">Spanish</string>
|
||||
<string name="settings_eeprom_language_italian">Italian</string>
|
||||
<string name="settings_eeprom_language_korean">Korean</string>
|
||||
<string name="settings_eeprom_language_chinese">Chinese</string>
|
||||
<string name="settings_eeprom_language_portuguese">Portuguese</string>
|
||||
<string name="settings_eeprom_video_standard">EEPROM Video Standard</string>
|
||||
<string name="settings_eeprom_video_standard_ntsc_m">NTSC-M</string>
|
||||
<string name="settings_eeprom_video_standard_ntsc_j">NTSC-J</string>
|
||||
<string name="settings_eeprom_video_standard_pal_i">PAL-I</string>
|
||||
<string name="settings_eeprom_video_standard_pal_m">PAL-M</string>
|
||||
<string name="settings_eeprom_status_ready">EEPROM file: %1$s</string>
|
||||
<string name="settings_eeprom_status_missing">EEPROM not found at %1$s. Launch a game once to create it.</string>
|
||||
<string name="settings_eeprom_status_invalid">EEPROM is invalid at %1$s.</string>
|
||||
<string name="settings_eeprom_status_error">Failed to read EEPROM at %1$s.</string>
|
||||
<string name="settings_eeprom_status_unknown">EEPROM file: %1$s (contains unknown values; saving will normalize them)</string>
|
||||
<string name="settings_eeprom_hint">Updates language and video standard in eeprom.bin. Restart emulation after saving.</string>
|
||||
|
||||
<string name="settings_saved">Settings saved</string>
|
||||
<string name="settings_saved_with_eeprom">Settings and EEPROM saved</string>
|
||||
<string name="settings_saved_eeprom_missing">Settings saved. Launch a game once, then edit EEPROM.</string>
|
||||
<string name="settings_saved_eeprom_failed">Settings saved, but EEPROM update failed</string>
|
||||
|
||||
<string name="in_game_menu_title">Game Menu</string>
|
||||
<string name="in_game_menu_resume">Resume</string>
|
||||
|
||||
Reference in New Issue
Block a user