Add support for internal firmware

This commit is contained in:
Rafael Caetano 2021-02-22 00:11:54 +00:00
parent 7c261c2424
commit 138213a8b0
29 changed files with 857 additions and 88 deletions

View File

@ -23,6 +23,7 @@
-keepclassmembers enum * { *; }
-keep class me.magnum.melonds.domain.model.RendererConfiguration { *; }
-keep class me.magnum.melonds.domain.model.FirmwareConfiguration { *; }
-keep class me.magnum.melonds.domain.model.EmulatorConfiguration { *; }
-keep class me.magnum.melonds.domain.model.ConsoleType { *; }
-keep class me.magnum.melonds.domain.model.MicSource { *; }

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,36 @@
Custom NDS ARM7/ARM9 BIOS replacement
Copyright (c) 2013, Gilead Kutnick
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1) Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2) Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
-- Info --
This archive contains source code and assembly for a custom BIOS replacement
for the Nintendo DS system. This code is in no way affiliated with Nintendo
and is not derived from Nintendo's BIOS implementation but has been implemented
using publically available documentation.
It can be assembled using the included Makefile along with a proper ARM gcc
toolchain. Change the first four lines to point to the proper toolchain of your
choice.

View File

@ -12,6 +12,7 @@
#define MAX_CHEAT_SIZE (2*64)
MelonDSAndroid::EmulatorConfiguration buildEmulatorConfiguration(JNIEnv* env, jobject emulatorConfiguration);
MelonDSAndroid::FirmwareConfiguration buildFirmwareConfiguration(JNIEnv* env, jobject firmwareConfiguration);
GPU::RenderSettings buildRenderSettings(JNIEnv* env, jobject renderSettings);
void* emulate(void*);
@ -310,6 +311,7 @@ MelonDSAndroid::EmulatorConfiguration buildEmulatorConfiguration(JNIEnv* env, jo
jclass consoleTypeEnumClass = env->FindClass("me/magnum/melonds/domain/model/ConsoleType");
jclass micSourceEnumClass = env->FindClass("me/magnum/melonds/domain/model/MicSource");
jboolean useCustomBios = env->GetBooleanField(emulatorConfiguration, env->GetFieldID(emulatorConfigurationClass, "useCustomBios", "Z"));
jstring dsConfigDir = (jstring) env->GetObjectField(emulatorConfiguration, env->GetFieldID(emulatorConfigurationClass, "dsConfigDirectory", "Ljava/lang/String;"));
jstring dsiConfigDir = (jstring) env->GetObjectField(emulatorConfiguration, env->GetFieldID(emulatorConfigurationClass, "dsiConfigDirectory", "Ljava/lang/String;"));
jfloat fastForwardMaxSpeed = env->GetFloatField(emulatorConfiguration, env->GetFieldID(emulatorConfigurationClass, "fastForwardSpeedMultiplier", "F"));
@ -321,9 +323,11 @@ MelonDSAndroid::EmulatorConfiguration buildEmulatorConfiguration(JNIEnv* env, jo
jint micSource = env->GetIntField(micSourceEnum, env->GetFieldID(micSourceEnumClass, "sourceValue", "I"));
const char* dsDir = env->GetStringUTFChars(dsConfigDir, JNI_FALSE);
const char* dsiDir = env->GetStringUTFChars(dsiConfigDir, JNI_FALSE);
jobject firmwareConfigurationObject = env->GetObjectField(emulatorConfiguration, env->GetFieldID(emulatorConfigurationClass, "firmwareConfiguration", "Lme/magnum/melonds/domain/model/FirmwareConfiguration;"));
jobject rendererConfigurationObject = env->GetObjectField(emulatorConfiguration, env->GetFieldID(emulatorConfigurationClass, "rendererConfiguration", "Lme/magnum/melonds/domain/model/RendererConfiguration;"));
MelonDSAndroid::EmulatorConfiguration finalEmulatorConfiguration;
finalEmulatorConfiguration.userInternalFirmwareAndBios = !useCustomBios;
finalEmulatorConfiguration.dsConfigDir = const_cast<char*>(dsDir);
finalEmulatorConfiguration.dsiConfigDir = const_cast<char*>(dsiDir);
finalEmulatorConfiguration.fastForwardSpeedMultiplier = fastForwardMaxSpeed;
@ -331,10 +335,40 @@ MelonDSAndroid::EmulatorConfiguration buildEmulatorConfiguration(JNIEnv* env, jo
finalEmulatorConfiguration.consoleType = consoleType;
finalEmulatorConfiguration.soundEnabled = soundEnabled;
finalEmulatorConfiguration.micSource = micSource;
finalEmulatorConfiguration.firmwareConfiguration = buildFirmwareConfiguration(env, firmwareConfigurationObject);
finalEmulatorConfiguration.renderSettings = buildRenderSettings(env, rendererConfigurationObject);
return finalEmulatorConfiguration;
}
MelonDSAndroid::FirmwareConfiguration buildFirmwareConfiguration(JNIEnv* env, jobject firmwareConfiguration) {
jclass firmwareConfigurationClass = env->GetObjectClass(firmwareConfiguration);
jstring nicknameString = (jstring) env->GetObjectField(firmwareConfiguration, env->GetFieldID(firmwareConfigurationClass, "nickname", "Ljava/lang/String;"));
jstring messageString = (jstring) env->GetObjectField(firmwareConfiguration, env->GetFieldID(firmwareConfigurationClass, "message", "Ljava/lang/String;"));
int language = env->GetIntField(firmwareConfiguration, env->GetFieldID(firmwareConfigurationClass, "language", "I"));
int colour = env->GetIntField(firmwareConfiguration, env->GetFieldID(firmwareConfigurationClass, "favouriteColour", "I"));
int birthdayDay = env->GetIntField(firmwareConfiguration, env->GetFieldID(firmwareConfigurationClass, "birthdayDay", "I"));
int birthdayMonth = env->GetIntField(firmwareConfiguration, env->GetFieldID(firmwareConfigurationClass, "birthdayMonth", "I"));
jboolean isCopy = JNI_FALSE;
const char* nickname = env->GetStringUTFChars(nicknameString, &isCopy);
const char* message = env->GetStringUTFChars(messageString, &isCopy);
MelonDSAndroid::FirmwareConfiguration finalFirmwareConfiguration;
strcpy(finalFirmwareConfiguration.username, nickname);
strcpy(finalFirmwareConfiguration.message, message);
finalFirmwareConfiguration.language = language;
finalFirmwareConfiguration.favouriteColour = colour;
finalFirmwareConfiguration.birthdayDay = birthdayDay;
finalFirmwareConfiguration.birthdayMonth = birthdayMonth;
if (isCopy) {
env->ReleaseStringUTFChars(nicknameString, nickname);
env->ReleaseStringUTFChars(messageString, message);
}
return finalFirmwareConfiguration;
}
GPU::RenderSettings buildRenderSettings(JNIEnv* env, jobject renderSettings) {
jclass renderSettingsClass = env->GetObjectClass(renderSettings);
jboolean threadedRendering = env->GetBooleanField(renderSettings, env->GetFieldID(renderSettingsClass, "threadedRendering", "Z"));

View File

@ -1,6 +1,7 @@
package me.magnum.melonds.domain.model
data class EmulatorConfiguration(
val useCustomBios: Boolean,
val dsConfigDirectory: String,
val dsiConfigDirectory: String,
val fastForwardSpeedMultiplier: Float,
@ -8,5 +9,6 @@ data class EmulatorConfiguration(
val consoleType: ConsoleType,
val soundEnabled: Boolean,
val micSource: MicSource,
val firmwareConfiguration: FirmwareConfiguration,
val rendererConfiguration: RendererConfiguration
)

View File

@ -0,0 +1,20 @@
package me.magnum.melonds.domain.model
enum class FirmwareColour {
GRAY,
BROWN,
RED,
PINK,
ORANGE,
YELLOW,
LIME,
GREEN,
DARK_GREEN,
TURQUOISE,
LIGHT_BLUE,
BLUE,
DARK_BLUE,
PURPLE,
VIOLET,
FUCHSIA
}

View File

@ -0,0 +1,3 @@
package me.magnum.melonds.domain.model
data class FirmwareConfiguration(val nickname: String, val message: String, val language: Int, val favouriteColour: Int, val birthdayMonth: Int, val birthdayDay: Int)

View File

@ -15,6 +15,8 @@ interface SettingsRepository {
fun getRomIconFiltering(): RomIconFiltering
fun getDefaultConsoleType(): ConsoleType
fun getFirmwareConfiguration(): FirmwareConfiguration
fun useCustomBios(): Boolean
fun getDsBiosDirectory(): Uri?
fun getDsiBiosDirectory(): Uri?
fun showBootScreen(): Boolean

View File

@ -43,11 +43,12 @@ class SharedPreferencesSettingsRepository(private val context: Context, private
override fun getEmulatorConfiguration(): EmulatorConfiguration {
val consoleType = getDefaultConsoleType()
val useCustomBios = useCustomBios()
val dsBiosDirUri = getDsBiosDirectory()
val dsiBiosDirUri = getDsiBiosDirectory()
// Ensure all BIOS dirs are set. DSi requires both dirs to be set
if (dsBiosDirUri == null || (consoleType == ConsoleType.DSi && dsiBiosDirUri == null))
if ((consoleType == ConsoleType.DS && useCustomBios && dsBiosDirUri == null) || (consoleType == ConsoleType.DSi && (dsBiosDirUri == null || dsiBiosDirUri == null)))
throw IllegalStateException("BIOS directory not set")
val dsBiosDirPath = FileUtils.getAbsolutePathFromSAFUri(context, dsBiosDirUri)
@ -56,6 +57,7 @@ class SharedPreferencesSettingsRepository(private val context: Context, private
val dsiConfigDirectoryPath = "$dsiBiosDirPath/"
return EmulatorConfiguration(
useCustomBios(),
dsConfigDirectoryPath,
dsiConfigDirectoryPath,
getFastForwardSpeedMultiplier(),
@ -63,6 +65,7 @@ class SharedPreferencesSettingsRepository(private val context: Context, private
consoleType,
isSoundEnabled(),
getMicSource(),
getFirmwareConfiguration(),
RendererConfiguration(
getVideoFiltering(),
isThreadedRenderingEnabled()
@ -95,6 +98,31 @@ class SharedPreferencesSettingsRepository(private val context: Context, private
return enumValueOfIgnoreCase(consoleTypePreference)
}
override fun getFirmwareConfiguration(): FirmwareConfiguration {
val birthdayPreference = preferences.getString("firmware_settings_birthday", "01/01")!!
val parts = birthdayPreference.split("/")
val birthday = if (parts.size != 2) {
Pair(1, 1)
} else {
val day = parts[0].toIntOrNull() ?: 1
val month = parts[1].toIntOrNull() ?: 1
Pair(day, month)
}
return FirmwareConfiguration(
preferences.getString("firmware_settings_nickname", "Player")!!,
preferences.getString("firmware_settings_message", "Hello!")!!,
preferences.getString("firmware_settings_language", "1")!!.toInt(),
preferences.getInt("firmware_settings_colour", 0),
birthday.first,
birthday.second,
)
}
override fun useCustomBios(): Boolean {
return preferences.getBoolean("use_custom_bios", false)
}
override fun getDsBiosDirectory(): Uri? {
val dirPreference = preferences.getStringSet("bios_dir", null)?.firstOrNull()
return dirPreference?.let { Uri.parse(it) }

View File

@ -71,6 +71,7 @@ class EmulatorViewModel @ViewModelInject constructor(
fun getEmulatorConfigurationForRom(rom: Rom): EmulatorConfiguration {
val baseConfiguration = settingsRepository.getEmulatorConfiguration()
return EmulatorConfiguration(
baseConfiguration.useCustomBios,
baseConfiguration.dsConfigDirectory,
baseConfiguration.dsiConfigDirectory,
baseConfiguration.fastForwardSpeedMultiplier,
@ -78,6 +79,7 @@ class EmulatorViewModel @ViewModelInject constructor(
getRomOptionOrDefault(rom.config.runtimeConsoleType, baseConfiguration.consoleType),
baseConfiguration.soundEnabled,
getRomOptionOrDefault(rom.config.runtimeMicSource, baseConfiguration.micSource),
baseConfiguration.firmwareConfiguration,
baseConfiguration.rendererConfiguration
)
}

View File

@ -74,11 +74,6 @@ class RomListActivity : AppCompatActivity() {
})
}
override fun onStart() {
super.onStart()
checkConfigDirectorySetup()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
if (menu == null)
return super.onCreateOptionsMenu(menu)

View File

@ -148,6 +148,10 @@ class RomListViewModel @ViewModelInject constructor(
fun getRomConfigurationDirStatus(rom: Rom): ConfigurationUtils.ConfigurationDirStatus {
val romTargetConsoleType = getRomTargetConsoleType(rom)
if (romTargetConsoleType == ConsoleType.DS && !settingsRepository.useCustomBios()) {
return ConfigurationUtils.ConfigurationDirStatus.VALID
}
return getConsoleConfigurationDirStatus(romTargetConsoleType)
}

View File

@ -0,0 +1,73 @@
package me.magnum.melonds.ui.settings
import android.net.Uri
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import me.magnum.melonds.R
import me.magnum.melonds.ui.settings.preferences.FirmwareBirthdayPreference
import me.magnum.melonds.ui.settings.preferences.StoragePickerPreference
import me.magnum.melonds.utils.FileUtils
abstract class BasePreferencesFragment : PreferenceFragmentCompat() {
protected companion object {
private val sBindPreferenceSummaryToValueListener = Preference.OnPreferenceChangeListener { preference, value ->
when (preference) {
is ListPreference -> {
// For list preferences, look up the correct display value in
// the preference's 'entries' list.
val index = preference.findIndexOfValue(value.toString())
// Set the summary to reflect the new value.
val summary = if (index >= 0)
preference.entries[index]
else
preference.getContext().getString(R.string.not_set)
preference.setSummary(summary)
}
is StoragePickerPreference -> {
if (value == null || value !is Set<*> || value.isEmpty())
preference.summary = preference.getContext().getString(R.string.not_set)
else {
val uris = value.mapNotNull { FileUtils.getAbsolutePathFromSAFUri(preference.context, Uri.parse(it as String)) }
preference.summary = uris.joinToString("\n")
}
}
is FirmwareBirthdayPreference -> {
val birthdayString = (value as String?) ?: "01/01"
preference.summary = birthdayString
}
else -> {
// For all other preferences, set the summary to the value's
// simple string representation.
preference.summary = value.toString()
}
}
true
}
fun bindPreferenceSummaryToValue(preference: Preference?) {
if (preference == null)
return
// Set the listener to watch for value changes.
preference.onPreferenceChangeListener = sBindPreferenceSummaryToValueListener
// Trigger the listener immediately with the preference's
// current value. Special handling for directory pickers since sets can't be converted
// to string
if (preference is StoragePickerPreference) {
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager.getDefaultSharedPreferences(preference.context)
.getStringSet(preference.key, null))
} else {
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager.getDefaultSharedPreferences(preference.context)
.getString(preference.key, null))
}
}
}
abstract fun getTitle(): String
}

View File

@ -0,0 +1,16 @@
package me.magnum.melonds.ui.settings
import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat
import me.magnum.melonds.R
class FirmwarePreferencesFragment : BasePreferencesFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_firmware_user_settings, rootKey)
bindPreferenceSummaryToValue(findPreference("firmware_settings_birthday"))
}
override fun getTitle(): String {
return getString(R.string.firmware_user_settings)
}
}

View File

@ -22,61 +22,7 @@ import java.math.RoundingMode
import java.util.*
@AndroidEntryPoint
class MainPreferencesFragment : PreferenceFragmentCompat() {
private companion object {
val sBindPreferenceSummaryToValueListener = Preference.OnPreferenceChangeListener { preference, value ->
when (preference) {
is ListPreference -> {
// For list preferences, look up the correct display value in
// the preference's 'entries' list.
val index = preference.findIndexOfValue(value.toString())
// Set the summary to reflect the new value.
val summary = if (index >= 0)
preference.entries[index]
else
preference.getContext().getString(R.string.not_set)
preference.setSummary(summary)
}
is StoragePickerPreference -> {
if (value == null || value !is Set<*> || value.isEmpty())
preference.summary = preference.getContext().getString(R.string.not_set)
else {
val uris = value.mapNotNull { FileUtils.getAbsolutePathFromSAFUri(preference.context, Uri.parse(it as String)) }
preference.summary = uris.joinToString("\n")
}
}
else -> {
// For all other preferences, set the summary to the value's
// simple string representation.
preference.summary = value.toString()
}
}
true
}
fun bindPreferenceSummaryToValue(preference: Preference?) {
if (preference == null)
return
// Set the listener to watch for value changes.
preference.onPreferenceChangeListener = sBindPreferenceSummaryToValueListener
// Trigger the listener immediately with the preference's
// current value. Special handling for directory pickers since sets can't be converted
// to string
if (preference is StoragePickerPreference) {
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager.getDefaultSharedPreferences(preference.context)
.getStringSet(preference.key, null))
} else {
sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
PreferenceManager.getDefaultSharedPreferences(preference.context)
.getString(preference.key, null))
}
}
}
class MainPreferencesFragment : BasePreferencesFragment() {
private val viewModel: SettingsViewModel by activityViewModels()
private lateinit var clearRomCachePreference: Preference
@ -89,9 +35,13 @@ class MainPreferencesFragment : PreferenceFragmentCompat() {
}
}
override fun getTitle(): String {
return getString(R.string.settings)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.pref_main, rootKey)
clearRomCachePreference = findPreference<Preference>("rom_cache_clear")!!
clearRomCachePreference = findPreference("rom_cache_clear")!!
val consoleTypePreference = findPreference<ListPreference>("console_type")!!
val dsBiosDirPreference = findPreference<StoragePickerPreference>("bios_dir")!!
val dsiBiosDirPreference = findPreference<StoragePickerPreference>("dsi_bios_dir")!!

View File

@ -10,6 +10,14 @@ class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupActionBar()
supportFragmentManager.addOnBackStackChangedListener {
val fragment = supportFragmentManager.fragments.lastOrNull()
if (fragment is BasePreferencesFragment) {
supportActionBar?.title = fragment.getTitle()
}
}
supportFragmentManager
.beginTransaction()
.replace(android.R.id.content, MainPreferencesFragment())
@ -24,11 +32,19 @@ class SettingsActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId
if (id == android.R.id.home) {
if (!super.onOptionsItemSelected(item)) {
if (!popBackStackIfNeeded()) {
finish()
}
return true
}
return super.onOptionsItemSelected(item)
}
private fun popBackStackIfNeeded(): Boolean {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack()
return true
}
return false
}
}

View File

@ -8,7 +8,9 @@ import android.view.View
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.content.res.getIntOrThrow
import androidx.core.view.isGone
import androidx.core.widget.ImageViewCompat
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import me.magnum.melonds.R
import me.magnum.melonds.domain.model.ConsoleType
@ -21,8 +23,8 @@ class BiosDirectoryPickerPreference(context: Context?, attrs: AttributeSet?) : S
selectionType = SelectionType.DIRECTORY
}
lateinit var consoleType: ConsoleType
lateinit var imageViewStatus: ImageView
private var consoleType: ConsoleType? = null
private var imageViewStatus: ImageView? = null
override fun onDirectoryPicked(uri: Uri?) {
super.onDirectoryPicked(uri)
@ -33,25 +35,37 @@ class BiosDirectoryPickerPreference(context: Context?, attrs: AttributeSet?) : S
updateStatusIndicator(uri)
}
override fun onDependencyChanged(dependency: Preference?, disableDependent: Boolean) {
super.onDependencyChanged(dependency, disableDependent)
imageViewStatus?.isGone = disableDependent
}
private fun updateStatusIndicator(uri: Uri?) {
val dirResult = ConfigurationUtils.checkConfigurationDirectory(context, uri, consoleType)
if (!isEnabled) {
imageViewStatus?.isGone = true
return
}
imageViewStatus?.isGone = false
val dirResult = ConfigurationUtils.checkConfigurationDirectory(context, uri, consoleType!!)
when (dirResult.status) {
ConfigurationUtils.ConfigurationDirStatus.VALID -> {
(imageViewStatus.parent as View).visibility = View.GONE
(imageViewStatus!!.parent as View).visibility = View.GONE
}
ConfigurationUtils.ConfigurationDirStatus.INVALID -> {
(imageViewStatus.parent as View).visibility = View.VISIBLE
imageViewStatus.setImageResource(R.drawable.ic_status_warn)
ImageViewCompat.setImageTintList(imageViewStatus, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.statusWarn)))
(imageViewStatus!!.parent as View).visibility = View.VISIBLE
imageViewStatus!!.setImageResource(R.drawable.ic_status_warn)
ImageViewCompat.setImageTintList(imageViewStatus!!, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.statusWarn)))
}
ConfigurationUtils.ConfigurationDirStatus.UNSET ->{
(imageViewStatus.parent as View).visibility = View.VISIBLE
imageViewStatus.setImageResource(R.drawable.ic_status_error)
ImageViewCompat.setImageTintList(imageViewStatus, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.statusError)))
(imageViewStatus!!.parent as View).visibility = View.VISIBLE
imageViewStatus!!.setImageResource(R.drawable.ic_status_error)
ImageViewCompat.setImageTintList(imageViewStatus!!, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.statusError)))
}
}
imageViewStatus.setOnClickListener {
FileStatusPopup(context, dirResult.fileResults).showAt(imageViewStatus)
imageViewStatus!!.setOnClickListener {
FileStatusPopup(context, dirResult.fileResults).showAt(imageViewStatus!!)
}
}
@ -60,13 +74,8 @@ class BiosDirectoryPickerPreference(context: Context?, attrs: AttributeSet?) : S
return
val attrArray = context.theme.obtainStyledAttributes(attrs, R.styleable.BiosDirectoryPickerPreference, 0, 0)
val count = attrArray.indexCount
for (i in 0..count) {
val attr = attrArray.getIndex(i)
when (attr) {
R.styleable.BiosDirectoryPickerPreference_consoleType -> consoleType = ConsoleType.values()[attrArray.getIntOrThrow(attr)]
}
}
consoleType = ConsoleType.values()[attrArray.getIntOrThrow(R.styleable.BiosDirectoryPickerPreference_consoleType)]
attrArray.recycle()
}

View File

@ -0,0 +1,127 @@
package me.magnum.melonds.ui.settings.preferences
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.preference.Preference
import me.magnum.melonds.R
import me.magnum.melonds.databinding.DialogFirmwareBirthdayBinding
import java.text.NumberFormat
class FirmwareBirthdayPreference(context: Context?, attrs: AttributeSet?) : Preference(context, attrs) {
companion object {
private val daysInMonth = mapOf(
1 to 31,
2 to 29,
3 to 31,
4 to 30,
5 to 31,
6 to 30,
7 to 31,
8 to 31,
9 to 30,
10 to 31,
11 to 30,
12 to 31
)
private val numberFormat = NumberFormat.getNumberInstance().apply {
minimumIntegerDigits = 2
}
}
override fun onClick() {
super.onClick()
val binding = DialogFirmwareBirthdayBinding.inflate(LayoutInflater.from(context))
AlertDialog.Builder(context)
.setTitle(title)
.setView(binding.root)
.setPositiveButton(R.string.ok) { dialog, _ ->
val day = binding.textBirthdayDay.text.toString().toInt()
val month = binding.textBirthdayMonth.text.toString().toInt()
val birthday = "${numberFormat.format(day)}/${numberFormat.format(month)}"
if (callChangeListener(birthday)) {
persistString(birthday)
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
val currentBirthday = getPersistedString("01/01")
val parts = currentBirthday.split("/")
if (parts.size != 2) {
setNumberFormatted(binding.textBirthdayDay, 1)
setNumberFormatted(binding.textBirthdayMonth, 1)
} else {
setNumberFormatted(binding.textBirthdayDay, parts[0].toIntOrNull() ?: 1)
setNumberFormatted(binding.textBirthdayMonth, parts[1].toIntOrNull() ?: 1)
}
binding.buttonBirthdayDayIncrease.setOnClickListener {
val day = binding.textBirthdayDay.text.toString().toIntOrNull() ?: 0
val month = binding.textBirthdayMonth.text.toString().toIntOrNull() ?: 1
val newDay = coerceDayForMonth(day + 1, month, true)
setNumberFormatted(binding.textBirthdayDay, newDay)
}
binding.buttonBirthdayDayDecrease.setOnClickListener {
val day = binding.textBirthdayDay.text.toString().toIntOrNull() ?: 2
val month = binding.textBirthdayMonth.text.toString().toIntOrNull() ?: 1
val newDay = coerceDayForMonth(day - 1, month, true)
setNumberFormatted(binding.textBirthdayDay, newDay)
}
binding.buttonBirthdayMonthIncrease.setOnClickListener {
val day = binding.textBirthdayDay.text.toString().toIntOrNull() ?: 1
val month = binding.textBirthdayMonth.text.toString().toIntOrNull() ?: 0
val newMonth = coerceMonth(month + 1)
val newDay = coerceDayForMonth(day, newMonth, false)
setNumberFormatted(binding.textBirthdayMonth, newMonth)
if (newDay != day) {
setNumberFormatted(binding.textBirthdayDay, newDay)
}
}
binding.buttonBirthdayMonthDecrease.setOnClickListener {
val day = binding.textBirthdayDay.text.toString().toIntOrNull() ?: 1
val month = binding.textBirthdayMonth.text.toString().toIntOrNull() ?: 2
val newMonth = coerceMonth(month - 1)
val newDay = coerceDayForMonth(day, newMonth, false)
setNumberFormatted(binding.textBirthdayMonth, newMonth)
if (newDay != day) {
setNumberFormatted(binding.textBirthdayDay, newDay)
}
}
}
private fun coerceDayForMonth(day: Int, month: Int, loop: Boolean): Int {
val daysInMonth = daysInMonth[month] ?: 1
return if (loop) {
when {
day > daysInMonth -> 1
day < 1 -> daysInMonth
else -> day
}
} else {
day.coerceIn(1, daysInMonth)
}
}
private fun coerceMonth(month: Int): Int {
return when {
month < 1 -> 12
month > 12 -> 1
else -> month
}
}
private fun setNumberFormatted(view: TextView, value: Int) {
val formattedValue = numberFormat.format(value)
view.text = formattedValue.toString()
}
}

View File

@ -0,0 +1,84 @@
package me.magnum.melonds.ui.settings.preferences
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.core.view.children
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import me.magnum.melonds.R
import me.magnum.melonds.databinding.DialogFirmwareColourPickerBinding
import me.magnum.melonds.domain.model.FirmwareColour
class FirmwareColourPickerPreference(context: Context?, attrs: AttributeSet?) : Preference(context, attrs) {
companion object {
private val colorMapper = mapOf(
FirmwareColour.GRAY to 0x61829A,
FirmwareColour.BROWN to 0xBA4900,
FirmwareColour.RED to 0xFB0018,
FirmwareColour.PINK to 0xFB8AFB,
FirmwareColour.ORANGE to 0xFB9200,
FirmwareColour.YELLOW to 0xF3E300,
FirmwareColour.LIME to 0xAAFB00,
FirmwareColour.GREEN to 0x00FB00,
FirmwareColour.DARK_GREEN to 0x00A238,
FirmwareColour.TURQUOISE to 0x49DB8A,
FirmwareColour.LIGHT_BLUE to 0x30BAF3,
FirmwareColour.BLUE to 0x0059F3,
FirmwareColour.DARK_BLUE to 0x000092,
FirmwareColour.PURPLE to 0x8A00D3,
FirmwareColour.VIOLET to 0xD300EB,
FirmwareColour.FUCHSIA to 0xFB0092
)
}
private lateinit var viewSelectedColour: View
init {
widgetLayoutResource = R.layout.preference_firmware_colour_picker_colour
}
override fun onClick() {
super.onClick()
val binding = DialogFirmwareColourPickerBinding.inflate(LayoutInflater.from(context))
val alertDialog = AlertDialog.Builder(context)
.setTitle(title)
.setView(binding.root)
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
binding.layoutGridColours.children.flatMap { (it as ViewGroup).children }.forEach {
it.setOnClickListener { view ->
val selectedColour = (view.tag as String).toInt()
updateSelectedColour(selectedColour)
if (callChangeListener(selectedColour)) {
persistInt(selectedColour)
}
alertDialog.dismiss()
}
}
}
private fun updateSelectedColour(selectedColour: Int) {
val firmwareColour = FirmwareColour.values()[selectedColour]
colorMapper[firmwareColour]?.let {
val colourWithAlpha = (0xFF000000 or it.toLong())
viewSelectedColour.setBackgroundColor(colourWithAlpha.toInt())
}
}
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
super.onBindViewHolder(holder)
if (holder != null) {
viewSelectedColour = holder.findViewById(R.id.viewSelectedColour)
updateSelectedColour(getPersistedInt(0))
}
}
}

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingTop="16dp"
android:paddingStart="?attr/dialogPreferredPadding"
android:paddingEnd="?attr/dialogPreferredPadding">
<Button
android:id="@+id/buttonBirthdayDayIncrease"
android:layout_width="50sp"
android:layout_height="wrap_content"
android:text="@string/symbol_increase"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/buttonBirthdayMonthIncrease"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textBirthdayDay"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold"
android:gravity="center_horizontal"
app:layout_constraintEnd_toEndOf="@+id/buttonBirthdayDayIncrease"
app:layout_constraintStart_toStartOf="@+id/buttonBirthdayDayIncrease"
app:layout_constraintTop_toBottomOf="@+id/buttonBirthdayDayIncrease"
tools:text="31" />
<Button
android:id="@+id/buttonBirthdayDayDecrease"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/symbol_decrease"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/textBirthdayDay"
app:layout_constraintStart_toStartOf="@+id/textBirthdayDay"
app:layout_constraintTop_toBottomOf="@+id/textBirthdayDay" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/day"
app:layout_constraintEnd_toEndOf="@id/buttonBirthdayDayDecrease"
app:layout_constraintStart_toStartOf="@+id/buttonBirthdayDayDecrease"
app:layout_constraintTop_toBottomOf="@+id/buttonBirthdayDayDecrease" />
<Button
android:id="@+id/buttonBirthdayMonthIncrease"
android:layout_width="50sp"
android:layout_height="wrap_content"
android:text="@string/symbol_increase"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/buttonBirthdayDayIncrease"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textBirthdayMonth"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold"
android:gravity="center_horizontal"
app:layout_constraintEnd_toEndOf="@+id/buttonBirthdayMonthIncrease"
app:layout_constraintStart_toStartOf="@+id/buttonBirthdayMonthIncrease"
app:layout_constraintTop_toBottomOf="@+id/buttonBirthdayMonthIncrease"
tools:text="12" />
<Button
android:id="@+id/buttonBirthdayMonthDecrease"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/symbol_decrease"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/textBirthdayMonth"
app:layout_constraintStart_toStartOf="@+id/textBirthdayMonth"
app:layout_constraintTop_toBottomOf="@+id/textBirthdayMonth" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/month"
app:layout_constraintEnd_toEndOf="@id/buttonBirthdayMonthDecrease"
app:layout_constraintStart_toStartOf="@+id/buttonBirthdayMonthDecrease"
app:layout_constraintTop_toBottomOf="@+id/buttonBirthdayMonthDecrease" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="?attr/dialogPreferredPadding"
android:paddingEnd="?attr/dialogPreferredPadding"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layoutGridColours"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/layoutColourRow0"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@+id/layoutColourRow1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<View
android:id="@+id/viewColour00"
style="@style/ColourTile"
android:background="#61829A"
android:tag="0" />
<View
android:id="@+id/viewColour01"
style="@style/ColourTile"
android:background="#BA4900"
android:tag="1" />
<View
android:id="@+id/viewColour02"
style="@style/ColourTile"
android:background="#FB0018"
android:tag="2" />
<View
android:id="@+id/viewColour03"
style="@style/ColourTile"
android:background="#FB8AFB"
android:tag="3" />
</LinearLayout>
<LinearLayout
android:id="@+id/layoutColourRow1"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/layoutColourRow2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layoutColourRow0">
<View
android:id="@+id/viewColour10"
style="@style/ColourTile"
android:background="#FB9200"
android:tag="4" />
<View
android:id="@+id/viewColour11"
style="@style/ColourTile"
android:background="#F3E300"
android:tag="5" />
<View
android:id="@+id/viewColour12"
style="@style/ColourTile"
android:background="#AAFB00"
android:tag="6" />
<View
android:id="@+id/viewColour13"
style="@style/ColourTile"
android:background="#00FB00"
android:tag="7" />
</LinearLayout>
<LinearLayout
android:id="@+id/layoutColourRow2"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/layoutColourRow3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layoutColourRow1">
<View
android:id="@+id/viewColour20"
style="@style/ColourTile"
android:background="#00A238"
android:tag="8"
app:layout_constraintBottom_toTopOf="@+id/viewColour30"
app:layout_constraintEnd_toStartOf="@+id/viewColour21" />
<View
android:id="@+id/viewColour21"
style="@style/ColourTile"
android:background="#49DB8A"
android:tag="9" />
<View
android:id="@+id/viewColour22"
style="@style/ColourTile"
android:background="#30BAF3"
android:tag="10" />
<View
android:id="@+id/viewColour23"
style="@style/ColourTile"
android:background="#0059F3"
android:tag="11" />
</LinearLayout>
<LinearLayout
android:id="@+id/layoutColourRow3"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layoutColourRow2">
<View
android:id="@+id/viewColour30"
style="@style/ColourTile"
android:background="#000092"
android:tag="12" />
<View
android:id="@+id/viewColour31"
style="@style/ColourTile"
android:background="#8A00D3"
android:tag="13" />
<View
android:id="@+id/viewColour32"
style="@style/ColourTile"
android:background="#D300EB"
android:tag="14" />
<View
android:id="@+id/viewColour33"
style="@style/ColourTile"
android:background="#FB0092"
android:tag="15" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<View
android:id="@+id/viewSelectedColour"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
tools:background="#FB9200"/>
</LinearLayout>

View File

@ -10,6 +10,15 @@
<item>dsi</item>
</string-array>
<string-array name="firmware_settings_language_values">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>0</item>
</string-array>
<string-array name="video_filtering_values">
<item>none</item>
<item>linear</item>

View File

@ -67,6 +67,10 @@
<string name="cache_size_calculating">Cache size: calculating…</string>
<string name="rom_search_directories">Search directories</string>
<string name="console_type">Default system</string>
<string name="firmware_user_settings">Firmware user settings</string>
<string name="firmware_user_settings_summary">Adjust the user settings of the internal firmware</string>
<string name="use_custom_bios_firmware">Use custom BIOS and firmware</string>
<string name="use_custom_bios_firmware_summary">Using a custom BIOS and firmware allows you to enter the DS\'s boot screen, just like in a real system.</string>
<string name="bios_directory">DS BIOS directory</string>
<string name="dsi_bios_directory">DSi BIOS directory</string>
<string name="show_boot_screen">Show boot screen</string>
@ -95,11 +99,21 @@
<string name="import_cheats_summary">Import cheats from database files to make them available. Only XML databases are supported for now. Any previously imported cheat will be deleted.</string>
<string name="rom_shortcut">ROM shortcut</string>
<string name="firmware_nickname">Nickname</string>
<string name="firmware_message">Message</string>
<string name="firmware_language">Language</string>
<string name="firmware_favourite_colour">Favourite colour</string>
<string name="firmware_birthday">Birthday</string>
<string name="error_clear_rom_cache">Failed to clear ROM cache</string>
<string name="importing_cheats">Importing cheats…</string>
<string name="starting">Starting…</string>
<string name="failed_save_cheat_changes">Could not save cheat changes</string>
<string name="no_cheats_found">Could not find cheats for the current ROM. Try importing a different cheat database.</string>
<string name="day">Day</string>
<string name="month">Month</string>
<string name="symbol_increase">+</string>
<string name="symbol_decrease">-</string>
<string name="rom_settings">Rom settings</string> <!-- Accessibility string. Should not use acronyms -->
<string name="label_rom_config_console">Boot system</string>
@ -155,6 +169,15 @@
<item>@string/console_dsi</item>
</string-array>
<string-array name="firmware_settings_language_options">
<item>English</item>
<item>French</item>
<item>German</item>
<item>Italian</item>
<item>Spanish</item>
<item>Japanese</item>
</string-array>
<string-array name="game_runtime_mic_source_options">
<item>Default</item>
<item>None</item>

View File

@ -75,4 +75,14 @@
<item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
<style name="ColourTile">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">match_parent</item>
<item name="android:layout_margin">16dp</item>
<item name="android:layout_weight">1</item>
<item name="android:gravity">center</item>
<item name="android:clickable">true</item>
<item name="android:focusable">true</item>
</style>
</resources>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<EditTextPreference
android:key="firmware_settings_nickname"
android:title="@string/firmware_nickname"
android:summary="%s"
app:useSimpleSummaryProvider="true"
android:defaultValue="Player" />
<EditTextPreference
android:key="firmware_settings_message"
android:title="@string/firmware_message"
android:summary="%s"
app:useSimpleSummaryProvider="true"
android:defaultValue="Hello!" />
<ListPreference
android:key="firmware_settings_language"
android:title="@string/firmware_language"
android:summary="%s"
android:entries="@array/firmware_settings_language_options"
android:entryValues="@array/firmware_settings_language_values"
android:defaultValue="1" />
<me.magnum.melonds.ui.settings.preferences.FirmwareColourPickerPreference
android:key="firmware_settings_colour"
android:title="@string/firmware_favourite_colour" />
<me.magnum.melonds.ui.settings.preferences.FirmwareBirthdayPreference
android:key="firmware_settings_birthday"
android:title="@string/firmware_birthday"
android:defaultValue="01/01" />
</PreferenceScreen>

View File

@ -57,9 +57,28 @@
android:entryValues="@array/console_type_values"
android:defaultValue="ds" />
<Preference
android:key="firmware_user_settings"
android:title="@string/firmware_user_settings"
android:summary="@string/firmware_user_settings_summary"
android:fragment="me.magnum.melonds.ui.settings.FirmwarePreferencesFragment" />
<SwitchPreference
android:key="use_custom_bios"
android:title="@string/use_custom_bios_firmware"
android:summary="@string/use_custom_bios_firmware_summary"
android:defaultValue="false" />
<SwitchPreference
android:key="show_bios"
android:title="@string/show_boot_screen"
android:dependency="use_custom_bios"
android:defaultValue="false" />
<me.magnum.melonds.ui.settings.preferences.BiosDirectoryPickerPreference
android:key="bios_dir"
android:title="@string/bios_directory"
android:dependency="use_custom_bios"
app:consoleType="ds" />
<me.magnum.melonds.ui.settings.preferences.BiosDirectoryPickerPreference
@ -67,11 +86,6 @@
android:title="@string/dsi_bios_directory"
app:consoleType="dsi" />
<SwitchPreference
android:key="show_bios"
android:title="@string/show_boot_screen"
android:defaultValue="false" />
<SwitchPreference
android:key="enable_jit"
android:title="@string/enable_jit"

@ -1 +1 @@
Subproject commit f29e1920bd8f58031ec1521750db9b44d0355148
Subproject commit 149a551669d81d6c350fb23df6ebb0c647d138b0