added EEPROM editor in settings change language and video standard

This commit is contained in:
izzy2lost
2026-03-05 05:08:24 -05:00
parent 370e54072d
commit aa9815909a
4 changed files with 395 additions and 1 deletions
@@ -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>