mirror of
https://github.com/Lime3DS/Lime3DS.git
synced 2026-04-08 18:31:29 -06:00
implement custom settings fully
# Conflicts: # src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt # src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt
This commit is contained in:
parent
1121a9e6c4
commit
a647cabda9
@ -87,15 +87,25 @@ class EmulationActivity : AppCompatActivity() {
|
||||
RefreshRateUtil.enforceRefreshRate(this, sixtyHz = true)
|
||||
|
||||
ThemeUtil.setTheme(this)
|
||||
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val game = try {
|
||||
intent.extras?.let { extras ->
|
||||
BundleCompat.getParcelable(extras, "game", Game::class.java)
|
||||
} ?: run {
|
||||
Log.error("[EmulationActivity] Missing game data in intent extras")
|
||||
return
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.error("[EmulationActivity] Failed to retrieve game data: ${e.message}")
|
||||
return
|
||||
}
|
||||
// load global settings if for some reason they aren't (should be loaded in MainActivity)
|
||||
if (Settings.settings.getAllGlobal().isEmpty()) {
|
||||
SettingsFile.loadSettings(Settings.settings)
|
||||
}
|
||||
// once per-game settings are added, load them here!
|
||||
// load per-game settings
|
||||
SettingsFile.loadSettings(Settings.settings, String.format("%016X", game.titleId))
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
secondaryDisplay = SecondaryDisplay(this, Settings.settings)
|
||||
secondaryDisplay.updateDisplay()
|
||||
@ -128,18 +138,6 @@ class EmulationActivity : AppCompatActivity() {
|
||||
|
||||
applyOrientationSettings() // Check for orientation settings at startup
|
||||
|
||||
val game = try {
|
||||
intent.extras?.let { extras ->
|
||||
BundleCompat.getParcelable(extras, "game", Game::class.java)
|
||||
} ?: run {
|
||||
Log.error("[EmulationActivity] Missing game data in intent extras")
|
||||
return
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.error("[EmulationActivity] Failed to retrieve game data: ${e.message}")
|
||||
return
|
||||
}
|
||||
|
||||
NativeLibrary.playTimeManagerStart(game.titleId)
|
||||
}
|
||||
|
||||
|
||||
@ -59,6 +59,8 @@ import org.citra.citra_emu.utils.FileUtil
|
||||
import org.citra.citra_emu.utils.GameIconUtils
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
|
||||
class GameAdapter(
|
||||
private val activity: AppCompatActivity,
|
||||
@ -485,6 +487,15 @@ class GameAdapter(
|
||||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
|
||||
bottomSheetView.findViewById<MaterialButton>(R.id.application_settings).setOnClickListener {
|
||||
SettingsActivity.launch(
|
||||
context,
|
||||
SettingsFile.FILE_NAME_CONFIG,
|
||||
String.format("%016X", holder.game.titleId)
|
||||
)
|
||||
bottomSheetDialog.dismiss()
|
||||
}
|
||||
|
||||
val compressDecompressButton = bottomSheetView.findViewById<MaterialButton>(R.id.compress_decompress)
|
||||
if (game.isInstalled) {
|
||||
compressDecompressButton.setOnClickListener {
|
||||
|
||||
@ -175,7 +175,7 @@ class Settings {
|
||||
KEY_BUTTON_RIGHT
|
||||
)
|
||||
val axisTitles = listOf(
|
||||
R.string.controller_axis_vertical,
|
||||
R.string.controller_axis_vertical,
|
||||
R.string.controller_axis_horizontal
|
||||
)
|
||||
val dPadTitles = listOf(
|
||||
|
||||
@ -7,7 +7,7 @@ package org.citra.citra_emu.features.settings.model
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class SettingsViewModel : ViewModel() {
|
||||
// the Settings Activity will always work with a local copy of settings while
|
||||
// editing, to avoid issues with conflicting active/edited settings
|
||||
// the settings activity primarily manipulates its own copy of the settings object
|
||||
// syncing it with the active settings only when saving
|
||||
val settings = Settings()
|
||||
}
|
||||
|
||||
@ -27,13 +27,26 @@ class SettingsActivityPresenter(private val activityView: SettingsActivityView,
|
||||
private var shouldSave = false
|
||||
private lateinit var menuTag: String
|
||||
private lateinit var gameId: String
|
||||
private var perGameInGlobalContext = false
|
||||
|
||||
fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) {
|
||||
this.menuTag = menuTag
|
||||
this.gameId = gameId
|
||||
// merge the active settings into the local settings activity instance
|
||||
|
||||
perGameInGlobalContext = gameId != "" && !Settings.settings.isPerGame()
|
||||
|
||||
// sync the active settings into my local settings appropriately
|
||||
// if we are editing global settings rom a game, this should just work
|
||||
// to sync only the global settings into the local version
|
||||
settings.gameId = gameId
|
||||
settings.mergeSettings(Settings.settings)
|
||||
|
||||
// if we are editing per-game settings when the game is not loaded,
|
||||
// we need to load the per-game settings now from the ini file
|
||||
if (perGameInGlobalContext) {
|
||||
SettingsFile.loadSettings(settings, gameId)
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
|
||||
}
|
||||
|
||||
@ -72,6 +72,7 @@ class SettingsAdapter(
|
||||
) : RecyclerView.Adapter<SettingViewHolder<SettingsItem>?>(), DialogInterface.OnClickListener,
|
||||
DialogInterface.OnMultiChoiceClickListener {
|
||||
private var settings: ArrayList<SettingsItem>? = null
|
||||
var isPerGame: Boolean = false
|
||||
private var clickedItem: SettingsItem? = null
|
||||
private var clickedPosition: Int
|
||||
private var dialog: AlertDialog? = null
|
||||
@ -226,6 +227,8 @@ class SettingsAdapter(
|
||||
if (fragmentView.activityView != null)
|
||||
// Reload the settings list to update the UI
|
||||
fragmentView.loadSettingsList()
|
||||
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
private fun onSingleChoiceClick(item: SingleChoiceSetting) {
|
||||
@ -543,12 +546,22 @@ class SettingsAdapter(
|
||||
}
|
||||
|
||||
fun <T> resetSettingToDefault(setting: AbstractSetting<T>, position: Int) {
|
||||
fragmentView.activityView?.settings?.set(setting,setting.defaultValue)
|
||||
val settings = fragmentView.activityView?.settings ?: return
|
||||
settings.set(setting,setting.defaultValue)
|
||||
notifyItemChanged(position)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.loadSettingsList()
|
||||
}
|
||||
|
||||
fun <T> resetSettingToGlobal(setting: AbstractSetting<T>, position: Int) {
|
||||
val settings = fragmentView.activityView?.settings ?: return
|
||||
settings.clearOverride(setting)
|
||||
notifyItemChanged(position)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.loadSettingsList()
|
||||
|
||||
}
|
||||
|
||||
fun onInputBindingLongClick(setting: InputBindingSetting, position: Int): Boolean {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setMessage(R.string.reset_setting_confirmation)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Citra Emulator Project
|
||||
// Copyright Citra Emulator Project / Azahar Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
|
||||
@ -66,6 +66,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
|
||||
fun onViewCreated(settingsAdapter: SettingsAdapter) {
|
||||
this.settingsAdapter = settingsAdapter
|
||||
preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
settingsAdapter.isPerGame = !TextUtils.isEmpty(gameId)
|
||||
loadSettingsList()
|
||||
}
|
||||
|
||||
|
||||
@ -55,6 +55,9 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
|
||||
binding.textSettingDescription.alpha = 0.5f
|
||||
binding.textSettingValue.alpha = 0.5f
|
||||
}
|
||||
|
||||
showGlobalButtonIfNeeded(binding.buttonUseGlobal, position)
|
||||
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
||||
@ -34,6 +34,8 @@ class MultiChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Settin
|
||||
binding.textSettingDescription.alpha = 0.5f
|
||||
binding.textSettingValue.alpha = 0.5f
|
||||
}
|
||||
|
||||
showGlobalButtonIfNeeded(binding.buttonUseGlobal, position)
|
||||
}
|
||||
|
||||
private fun getTextSetting(): String {
|
||||
|
||||
@ -37,4 +37,22 @@ abstract class SettingViewHolder<out T: SettingsItem>(itemView: View, protected
|
||||
abstract override fun onClick(clicked: View)
|
||||
|
||||
abstract override fun onLongClick(clicked: View): Boolean
|
||||
|
||||
fun showGlobalButtonIfNeeded(buttonUseGlobal: View, position: Int) {
|
||||
setting ?: return
|
||||
// Show "Revert to global" button in Custom Settings if applicable.
|
||||
val settings = adapter.fragmentView.activityView?.settings
|
||||
val showGlobal = settings?.isPerGame() == true
|
||||
&& setting?.setting != null
|
||||
&& settings.hasOverride(setting!!.setting!!)
|
||||
|
||||
buttonUseGlobal.visibility = if (showGlobal) View.VISIBLE else View.GONE
|
||||
if (showGlobal) {
|
||||
buttonUseGlobal.setOnClickListener {
|
||||
setting?.setting?.let { descriptor ->
|
||||
adapter.resetSettingToGlobal(descriptor, bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,8 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
|
||||
binding.textSettingDescription.alpha = 0.5f
|
||||
binding.textSettingValue.alpha = 0.5f
|
||||
}
|
||||
|
||||
showGlobalButtonIfNeeded(binding.buttonUseGlobal, position)
|
||||
}
|
||||
|
||||
private fun getTextSetting(): String {
|
||||
|
||||
@ -36,6 +36,8 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
|
||||
binding.textSettingDescription.alpha = 0.5f
|
||||
binding.textSettingValue.alpha = 0.5f
|
||||
}
|
||||
|
||||
showGlobalButtonIfNeeded(binding.buttonUseGlobal, position)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
||||
@ -34,6 +34,8 @@ class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: Settin
|
||||
binding.textSettingDescription.alpha = 0.5f
|
||||
binding.textSettingValue.alpha = 0.5f
|
||||
}
|
||||
|
||||
showGlobalButtonIfNeeded(binding.buttonUseGlobal, position)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
||||
@ -38,6 +38,8 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
|
||||
val textAlpha = if (setting.isActive) 1f else 0.5f
|
||||
binding.textSettingName.alpha = textAlpha
|
||||
binding.textSettingDescription.alpha = textAlpha
|
||||
|
||||
showGlobalButtonIfNeeded(binding.buttonUseGlobal, position)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
|
||||
@ -35,7 +35,6 @@ import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.Insets
|
||||
@ -91,7 +90,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
private lateinit var emulationState: EmulationState
|
||||
private var perfStatsUpdater: Runnable? = null
|
||||
|
||||
private lateinit var emulationActivity: EmulationActivity
|
||||
private var emulationActivity: EmulationActivity? = null
|
||||
|
||||
private var _binding: FragmentEmulationBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
@ -378,8 +377,31 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
R.id.menu_settings -> {
|
||||
SettingsActivity.launch(
|
||||
requireContext(),
|
||||
SettingsFile.FILE_NAME_CONFIG, null
|
||||
SettingsFile.FILE_NAME_CONFIG,
|
||||
""
|
||||
)
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
R.id.menu_application_settings -> {
|
||||
val titleId = NativeLibrary.getRunningTitleId()
|
||||
if (titleId != 0L) {
|
||||
val gameId = java.lang.String.format("%016X", titleId)
|
||||
SettingsActivity.launch(
|
||||
requireContext(),
|
||||
SettingsFile.FILE_NAME_CONFIG,
|
||||
gameId
|
||||
)
|
||||
} else {
|
||||
// Fallback: open global settings if title id unknown
|
||||
SettingsActivity.launch(
|
||||
requireContext(),
|
||||
SettingsFile.FILE_NAME_CONFIG,
|
||||
""
|
||||
)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@ -517,7 +539,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
}
|
||||
|
||||
if (DirectoryInitialization.areCitraDirectoriesReady()) {
|
||||
emulationState.run(emulationActivity.isActivityRecreated)
|
||||
emulationState.run(emulationActivity!!.isActivityRecreated)
|
||||
} else {
|
||||
setupCitraDirectoriesThenStartEmulation()
|
||||
}
|
||||
@ -532,6 +554,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
emulationActivity = null
|
||||
NativeLibrary.clearEmulationActivity()
|
||||
super.onDetach()
|
||||
}
|
||||
@ -551,7 +574,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
if (directoryInitializationState ===
|
||||
DirectoryInitializationState.CITRA_DIRECTORIES_INITIALIZED
|
||||
) {
|
||||
emulationState.run(emulationActivity.isActivityRecreated)
|
||||
emulationState.run(emulationActivity!!.isActivityRecreated)
|
||||
} else if (directoryInitializationState ===
|
||||
DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED
|
||||
) {
|
||||
@ -870,7 +893,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
|
||||
popupMenu.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_emulation_amiibo_load -> {
|
||||
emulationActivity.openAmiiboFileLauncher.launch(false)
|
||||
emulationActivity!!.openAmiiboFileLauncher.launch(false)
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
|
||||
class EmulationViewModel : ViewModel() {
|
||||
val emulationStarted get() = _emulationStarted.asStateFlow()
|
||||
@ -26,6 +27,11 @@ class EmulationViewModel : ViewModel() {
|
||||
private val _shaderMessage = MutableStateFlow("")
|
||||
|
||||
|
||||
/** Used for the initial load of settings. Call rebuild for later creations. */
|
||||
fun loadSettings(titleId: Long) {
|
||||
if (settings.getAllGlobal().isNotEmpty()) return //already loaded
|
||||
SettingsFile.loadSettings(settings, String.format("%016X", titleId))
|
||||
}
|
||||
fun setShaderProgress(progress: Int) {
|
||||
_shaderProgress.value = progress
|
||||
}
|
||||
|
||||
@ -79,28 +79,65 @@ static const std::array<int, Settings::NativeAnalog::NumAnalogs> default_analogs
|
||||
}};
|
||||
|
||||
template <>
|
||||
void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) {
|
||||
std::string setting_value =
|
||||
android_config->Get(group, setting.GetLabel(), setting.GetDefault());
|
||||
std::string Config::GetSetting(const std::string& group, Settings::Setting<std::string>& setting) {
|
||||
std::string setting_value = setting.GetDefault();
|
||||
if (per_game_config && per_game_config->HasValue(group, setting.GetLabel())) {
|
||||
setting_value = per_game_config->Get(group, setting.GetLabel(), setting_value);
|
||||
} else if (android_config) {
|
||||
setting_value = android_config->Get(group, setting.GetLabel(), setting_value);
|
||||
}
|
||||
if (setting_value.empty()) {
|
||||
setting_value = setting.GetDefault();
|
||||
}
|
||||
setting = std::move(setting_value);
|
||||
return setting_value;
|
||||
}
|
||||
|
||||
template <>
|
||||
void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) {
|
||||
setting = std::move(GetSetting(group, setting));
|
||||
}
|
||||
|
||||
template <>
|
||||
bool Config::GetSetting(const std::string& group, Settings::Setting<bool>& setting) {
|
||||
bool value = setting.GetDefault();
|
||||
if (per_game_config && per_game_config->HasValue(group, setting.GetLabel())) {
|
||||
value = per_game_config->GetBoolean(group, setting.GetLabel(), value);
|
||||
} else if (android_config) {
|
||||
value = android_config->GetBoolean(group, setting.GetLabel(), value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
template <>
|
||||
void Config::ReadSetting(const std::string& group, Settings::Setting<bool>& setting) {
|
||||
setting = android_config->GetBoolean(group, setting.GetLabel(), setting.GetDefault());
|
||||
setting = GetSetting(group, setting);
|
||||
}
|
||||
|
||||
//TODO: figure out why ranged isn't being used
|
||||
template <typename Type, bool ranged>
|
||||
Type Config::GetSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
|
||||
if constexpr (std::is_floating_point_v<Type>) {
|
||||
double value = static_cast<double>(setting.GetDefault());
|
||||
if (per_game_config && per_game_config->HasValue(group, setting.GetLabel())) {
|
||||
value = per_game_config->GetReal(group, setting.GetLabel(), value);
|
||||
} else if (android_config) {
|
||||
value = android_config->GetReal(group, setting.GetLabel(), value);
|
||||
}
|
||||
return static_cast<Type>(value);
|
||||
} else {
|
||||
long value = static_cast<long>(setting.GetDefault());
|
||||
if (per_game_config && per_game_config->HasValue(group, setting.GetLabel())) {
|
||||
value = per_game_config->GetInteger(group, setting.GetLabel(), value);
|
||||
} else if (android_config) {
|
||||
value = android_config->GetInteger(group, setting.GetLabel(), value);
|
||||
}
|
||||
return static_cast<Type>(value);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Type, bool ranged>
|
||||
void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
|
||||
if constexpr (std::is_floating_point_v<Type>) {
|
||||
setting = android_config->GetReal(group, setting.GetLabel(), setting.GetDefault());
|
||||
} else {
|
||||
setting = static_cast<Type>(android_config->GetInteger(
|
||||
group, setting.GetLabel(), static_cast<long>(setting.GetDefault())));
|
||||
}
|
||||
setting = GetSetting(group, setting);
|
||||
}
|
||||
|
||||
void Config::ReadValues() {
|
||||
@ -139,9 +176,8 @@ void Config::ReadValues() {
|
||||
ReadSetting("Core", Settings::values.cpu_clock_percentage);
|
||||
|
||||
// Renderer
|
||||
Settings::values.use_gles = android_config->GetBoolean("Renderer", "use_gles", true);
|
||||
Settings::values.shaders_accurate_mul =
|
||||
android_config->GetBoolean("Renderer", "shaders_accurate_mul", false);
|
||||
ReadSetting("Renderer",Settings::values.use_gles);
|
||||
ReadSetting("Renderer",Settings::values.shaders_accurate_mul);
|
||||
ReadSetting("Renderer", Settings::values.graphics_api);
|
||||
ReadSetting("Renderer", Settings::values.async_presentation);
|
||||
ReadSetting("Renderer", Settings::values.async_shader_compilation);
|
||||
@ -156,7 +192,14 @@ void Config::ReadValues() {
|
||||
ReadSetting("Renderer", Settings::values.texture_sampling);
|
||||
ReadSetting("Renderer", Settings::values.turbo_limit);
|
||||
// Workaround to map Android setting for enabling the frame limiter to the format Citra expects
|
||||
if (android_config->GetBoolean("Renderer", "use_frame_limit", true)) {
|
||||
// TODO: test this!
|
||||
bool use_frame_limit = false;
|
||||
if (per_game_config && per_game_config->HasValue("Renderer", "use_frame_limit")) {
|
||||
use_frame_limit = per_game_config->GetBoolean("Renderer", "use_frame_limit", true);
|
||||
} else if (android_config) {
|
||||
use_frame_limit = android_config->GetBoolean("Renderer", "use_frame_limit", true);
|
||||
}
|
||||
if (use_frame_limit) {
|
||||
ReadSetting("Renderer", Settings::values.frame_limit);
|
||||
} else {
|
||||
Settings::values.frame_limit = 0;
|
||||
@ -183,21 +226,10 @@ void Config::ReadValues() {
|
||||
ReadSetting("Renderer", Settings::values.swap_eyes_3d);
|
||||
ReadSetting("Renderer", Settings::values.render_3d_which_display);
|
||||
// Layout
|
||||
// Somewhat inelegant solution to ensure layout value is between 0 and 5 on read
|
||||
// since older config files may have other values
|
||||
int layoutInt = (int)android_config->GetInteger(
|
||||
"Layout", "layout_option", static_cast<int>(Settings::LayoutOption::LargeScreen));
|
||||
if (layoutInt < 0 || layoutInt > 5) {
|
||||
layoutInt = static_cast<int>(Settings::LayoutOption::LargeScreen);
|
||||
}
|
||||
Settings::values.layout_option = static_cast<Settings::LayoutOption>(layoutInt);
|
||||
Settings::values.screen_gap =
|
||||
static_cast<int>(android_config->GetReal("Layout", "screen_gap", 0));
|
||||
Settings::values.large_screen_proportion =
|
||||
static_cast<float>(android_config->GetReal("Layout", "large_screen_proportion", 2.25));
|
||||
Settings::values.small_screen_position = static_cast<Settings::SmallScreenPosition>(
|
||||
android_config->GetInteger("Layout", "small_screen_position",
|
||||
static_cast<int>(Settings::SmallScreenPosition::TopRight)));
|
||||
|
||||
ReadSetting("Layout",Settings::values.layout_option);
|
||||
ReadSetting("Layout",Settings::values.screen_gap);
|
||||
ReadSetting("Layout", Settings::values.small_screen_position);
|
||||
ReadSetting("Layout", Settings::values.screen_gap);
|
||||
ReadSetting("Layout", Settings::values.custom_top_x);
|
||||
ReadSetting("Layout", Settings::values.custom_top_y);
|
||||
@ -212,14 +244,8 @@ void Config::ReadValues() {
|
||||
ReadSetting("Layout", Settings::values.cardboard_x_shift);
|
||||
ReadSetting("Layout", Settings::values.cardboard_y_shift);
|
||||
ReadSetting("Layout", Settings::values.upright_screen);
|
||||
|
||||
Settings::values.portrait_layout_option =
|
||||
static_cast<Settings::PortraitLayoutOption>(android_config->GetInteger(
|
||||
"Layout", "portrait_layout_option",
|
||||
static_cast<int>(Settings::PortraitLayoutOption::PortraitTopFullWidth)));
|
||||
Settings::values.secondary_display_layout = static_cast<Settings::SecondaryDisplayLayout>(
|
||||
android_config->GetInteger("Layout", Settings::HKeys::secondary_display_layout.c_str(),
|
||||
static_cast<int>(Settings::SecondaryDisplayLayout::None)));
|
||||
ReadSetting("Layout", Settings::values.portrait_layout_option);
|
||||
ReadSetting("Layout", Settings::values.secondary_display_layout);
|
||||
ReadSetting("Layout", Settings::values.custom_portrait_top_x);
|
||||
ReadSetting("Layout", Settings::values.custom_portrait_top_y);
|
||||
ReadSetting("Layout", Settings::values.custom_portrait_top_width);
|
||||
@ -349,3 +375,37 @@ void Config::Reload() {
|
||||
LoadINI(DefaultINI::android_config_default_file_content);
|
||||
ReadValues();
|
||||
}
|
||||
|
||||
void Config::LoadPerGameConfig(u64 title_id, const std::string& fallback_name) {
|
||||
// Determine file name
|
||||
std::string name;
|
||||
if (title_id != 0) {
|
||||
std::ostringstream ss;
|
||||
ss << std::uppercase << std::hex << std::setw(16) << std::setfill('0') << title_id;
|
||||
name = ss.str();
|
||||
} else {
|
||||
name = fallback_name;
|
||||
}
|
||||
if (name.empty()) {
|
||||
per_game_config.reset();
|
||||
per_game_config_loc.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto base = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir);
|
||||
per_game_config_loc = base + "custom/" + name + ".ini";
|
||||
|
||||
std::string ini_buffer;
|
||||
FileUtil::ReadFileToString(true, per_game_config_loc, ini_buffer);
|
||||
if (!ini_buffer.empty()) {
|
||||
per_game_config = std::make_unique<INIReader>(ini_buffer.c_str(), ini_buffer.size());
|
||||
if (per_game_config->ParseError() < 0) {
|
||||
per_game_config.reset();
|
||||
}
|
||||
} else {
|
||||
per_game_config.reset();
|
||||
}
|
||||
|
||||
// Re-apply values so that per-game overrides (if any) take effect immediately.
|
||||
ReadValues();
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@ class Config {
|
||||
private:
|
||||
std::unique_ptr<INIReader> android_config;
|
||||
std::string android_config_loc;
|
||||
std::unique_ptr<INIReader> per_game_config;
|
||||
std::string per_game_config_loc;
|
||||
|
||||
bool LoadINI(const std::string& default_contents = "", bool retry = true);
|
||||
void ReadValues();
|
||||
@ -23,14 +25,27 @@ public:
|
||||
~Config();
|
||||
|
||||
void Reload();
|
||||
// Load a per-game config overlay by title id or fallback name. Does not create files.
|
||||
void LoadPerGameConfig(u64 title_id, const std::string& fallback_name = "");
|
||||
|
||||
private:
|
||||
/**
|
||||
* Applies a value read from the android_config to a Setting.
|
||||
*
|
||||
* @param group The name of the INI group
|
||||
* @param setting The yuzu setting to modify
|
||||
* @param setting The setting to modify
|
||||
*/
|
||||
template <typename Type, bool ranged>
|
||||
void ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting);
|
||||
|
||||
/**
|
||||
* Reads a value honoring per_game config, and returns it.
|
||||
* Does not modify the setting.
|
||||
*
|
||||
* @param group The name of the INI group
|
||||
* @param setting The setting to modify
|
||||
*/
|
||||
template <typename Type, bool ranged>
|
||||
Type GetSetting(const std::string& group, Settings::Setting<Type, ranged>& setting);
|
||||
|
||||
};
|
||||
|
||||
@ -191,6 +191,28 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
|
||||
|
||||
const auto graphics_api = Settings::values.graphics_api.GetValue();
|
||||
EGLContext* shared_context;
|
||||
|
||||
|
||||
// Load game-specific settings overlay if available
|
||||
u64 program_id{};
|
||||
FileUtil::SetCurrentRomPath(filepath);
|
||||
auto app_loader = Loader::GetLoader(filepath);
|
||||
if (app_loader) {
|
||||
app_loader->ReadProgramId(program_id);
|
||||
system.RegisterAppLoaderEarly(app_loader);
|
||||
}
|
||||
|
||||
// Forces a config reload on game boot, if the user changed settings in the UI
|
||||
Config global_config{};
|
||||
|
||||
// Use filename as fallback if title id is zero (e.g., homebrew)
|
||||
const std::string fallback_name =
|
||||
program_id == 0 ? std::string(FileUtil::GetFilename(filepath)) : std::string{};
|
||||
global_config.LoadPerGameConfig(program_id, fallback_name);
|
||||
system.ApplySettings();
|
||||
Settings::LogSettings();
|
||||
|
||||
|
||||
switch (graphics_api) {
|
||||
#ifdef ENABLE_OPENGL
|
||||
case Settings::GraphicsAPI::OpenGL:
|
||||
@ -228,18 +250,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Forces a config reload on game boot, if the user changed settings in the UI
|
||||
Config{};
|
||||
// Replace with game-specific settings
|
||||
u64 program_id{};
|
||||
FileUtil::SetCurrentRomPath(filepath);
|
||||
auto app_loader = Loader::GetLoader(filepath);
|
||||
if (app_loader) {
|
||||
app_loader->ReadProgramId(program_id);
|
||||
system.RegisterAppLoaderEarly(app_loader);
|
||||
}
|
||||
system.ApplySettings();
|
||||
Settings::LogSettings();
|
||||
|
||||
|
||||
Camera::RegisterFactory("image", std::make_unique<Camera::StillImage::Factory>());
|
||||
|
||||
@ -913,13 +924,18 @@ void Java_org_citra_citra_1emu_NativeLibrary_logUserDirectory(JNIEnv* env,
|
||||
|
||||
void Java_org_citra_citra_1emu_NativeLibrary_reloadSettings([[maybe_unused]] JNIEnv* env,
|
||||
[[maybe_unused]] jobject obj) {
|
||||
Config{};
|
||||
Config cfg{};
|
||||
Core::System& system{Core::System::GetInstance()};
|
||||
|
||||
// Replace with game-specific settings
|
||||
// Load game-specific settings overlay (if a game is running)
|
||||
if (system.IsPoweredOn()) {
|
||||
u64 program_id{};
|
||||
system.GetAppLoader().ReadProgramId(program_id);
|
||||
// Use the registered ROM path (if any) to derive a fallback name
|
||||
const std::string current_rom_path = FileUtil::GetCurrentRomPath();
|
||||
const std::string fallback_name =
|
||||
program_id == 0 ? std::string(FileUtil::GetFilename(current_rom_path)) : std::string{};
|
||||
cfg.LoadPerGameConfig(program_id, fallback_name);
|
||||
}
|
||||
|
||||
system.ApplySettings();
|
||||
|
||||
@ -180,6 +180,15 @@
|
||||
android:contentDescription="@string/cheats"
|
||||
android:text="@string/cheats" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/application_settings"
|
||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/application_settings"
|
||||
android:text="@string/application_settings" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/insert_cartridge_button"
|
||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||
|
||||
@ -62,6 +62,15 @@
|
||||
android:textSize="13sp"
|
||||
tools:text="1x" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_use_global"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/use_global"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@ -46,6 +46,15 @@
|
||||
android:textAlignment="viewStart"
|
||||
tools:text="@string/frame_limit_enable_description" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_use_global"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/use_global"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@ -57,6 +57,11 @@
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/preferences_settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_application_settings"
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/application_settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_exit"
|
||||
android:icon="@drawable/ic_exit"
|
||||
|
||||
@ -522,6 +522,8 @@
|
||||
<string name="menu_emulation_amiibo">Amiibo</string>
|
||||
<string name="menu_emulation_amiibo_load">Load</string>
|
||||
<string name="menu_emulation_amiibo_remove">Remove</string>
|
||||
<string name="application_settings">Custom Settings</string>
|
||||
<string name="use_global">Customized: Revert to Global</string>
|
||||
<string name="select_amiibo">Select Amiibo File</string>
|
||||
<string name="amiibo_load_error">Error Loading Amiibo</string>
|
||||
<string name="amiibo_load_error_message">While loading the specified Amiibo file, an error occurred. Please check that the file is correct.</string>
|
||||
|
||||
@ -1037,6 +1037,10 @@ void SetCurrentRomPath(const std::string& path) {
|
||||
g_currentRomPath = path;
|
||||
}
|
||||
|
||||
std::string GetCurrentRomPath() {
|
||||
return g_currentRomPath;
|
||||
}
|
||||
|
||||
bool StringReplace(std::string& haystack, const std::string& a, const std::string& b, bool swap) {
|
||||
const auto& needle = swap ? b : a;
|
||||
const auto& replacement = swap ? a : b;
|
||||
|
||||
@ -213,6 +213,7 @@ bool SetCurrentDir(const std::string& directory);
|
||||
void SetUserPath(const std::string& path = "");
|
||||
|
||||
void SetCurrentRomPath(const std::string& path);
|
||||
[[nodiscard]] std::string GetCurrentRomPath();
|
||||
|
||||
// Returns a pointer to a string with a Citra data dir in the user's home
|
||||
// directory. To be used in "multi-user" mode (that is, installed).
|
||||
|
||||
Loading…
Reference in New Issue
Block a user