diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index c9d6c9e3f..c764054e6 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -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) } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt index d43ea5a60..d3319d3c9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt @@ -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(R.id.application_settings).setOnClickListener { + SettingsActivity.launch( + context, + SettingsFile.FILE_NAME_CONFIG, + String.format("%016X", holder.game.titleId) + ) + bottomSheetDialog.dismiss() + } + val compressDecompressButton = bottomSheetView.findViewById(R.id.compress_decompress) if (game.isInstalled) { compressDecompressButton.setOnClickListener { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt index 5ef881bfd..c2b54d12e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt @@ -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( diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt index 166c74d16..e0f4f6e48 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt @@ -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() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt index 1c2cbf144..095241097 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt @@ -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) } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index c124cf383..f08ebc07c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -72,6 +72,7 @@ class SettingsAdapter( ) : RecyclerView.Adapter?>(), DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener { private var settings: ArrayList? = 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 resetSettingToDefault(setting: AbstractSetting, 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 resetSettingToGlobal(setting: AbstractSetting, 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) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt index 4521bae95..02827d659 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt @@ -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. diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index aaa067af2..e9506b44b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -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() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt index 69dd3fc0b..3d3102b95 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt @@ -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) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt index e94932b42..d9f08896d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt @@ -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 { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt index 2b408abe0..02a4df748 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt @@ -37,4 +37,22 @@ abstract class SettingViewHolder(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) + } + } + } + } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt index 8c764b617..ed16d8952 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt @@ -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 { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt index 1d651046e..7050df9cf 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt @@ -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) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt index 75981f538..de5296f34 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt @@ -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) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt index c92b7c6b7..8346f0fdb 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt @@ -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) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index a1538076c..005203ff8 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -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 } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt index 68458254e..3fc912671 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/EmulationViewModel.kt @@ -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 } diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index e0fa260c5..654d94b3e 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -79,28 +79,65 @@ static const std::array default_analogs }}; template <> -void Config::ReadSetting(const std::string& group, Settings::Setting& setting) { - std::string setting_value = - android_config->Get(group, setting.GetLabel(), setting.GetDefault()); +std::string Config::GetSetting(const std::string& group, Settings::Setting& 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& setting) { + setting = std::move(GetSetting(group, setting)); +} + +template <> +bool Config::GetSetting(const std::string& group, Settings::Setting& 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& setting) { - setting = android_config->GetBoolean(group, setting.GetLabel(), setting.GetDefault()); + setting = GetSetting(group, setting); +} + +//TODO: figure out why ranged isn't being used +template +Type Config::GetSetting(const std::string& group, Settings::Setting& setting) { + if constexpr (std::is_floating_point_v) { + double value = static_cast(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(value); + } else { + long value = static_cast(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(value); + } } template void Config::ReadSetting(const std::string& group, Settings::Setting& setting) { - if constexpr (std::is_floating_point_v) { - setting = android_config->GetReal(group, setting.GetLabel(), setting.GetDefault()); - } else { - setting = static_cast(android_config->GetInteger( - group, setting.GetLabel(), static_cast(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(Settings::LayoutOption::LargeScreen)); - if (layoutInt < 0 || layoutInt > 5) { - layoutInt = static_cast(Settings::LayoutOption::LargeScreen); - } - Settings::values.layout_option = static_cast(layoutInt); - Settings::values.screen_gap = - static_cast(android_config->GetReal("Layout", "screen_gap", 0)); - Settings::values.large_screen_proportion = - static_cast(android_config->GetReal("Layout", "large_screen_proportion", 2.25)); - Settings::values.small_screen_position = static_cast( - android_config->GetInteger("Layout", "small_screen_position", - static_cast(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(android_config->GetInteger( - "Layout", "portrait_layout_option", - static_cast(Settings::PortraitLayoutOption::PortraitTopFullWidth))); - Settings::values.secondary_display_layout = static_cast( - android_config->GetInteger("Layout", Settings::HKeys::secondary_display_layout.c_str(), - static_cast(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(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(); +} diff --git a/src/android/app/src/main/jni/config.h b/src/android/app/src/main/jni/config.h index 9d40295c4..b2989bd56 100644 --- a/src/android/app/src/main/jni/config.h +++ b/src/android/app/src/main/jni/config.h @@ -14,6 +14,8 @@ class Config { private: std::unique_ptr android_config; std::string android_config_loc; + std::unique_ptr 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 void ReadSetting(const std::string& group, Settings::Setting& 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 + Type GetSetting(const std::string& group, Settings::Setting& setting); + }; diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index f4a30610c..e3412891c 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -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()); @@ -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(); diff --git a/src/android/app/src/main/res/layout/dialog_about_game.xml b/src/android/app/src/main/res/layout/dialog_about_game.xml index cb7eea8de..2cac7adc7 100644 --- a/src/android/app/src/main/res/layout/dialog_about_game.xml +++ b/src/android/app/src/main/res/layout/dialog_about_game.xml @@ -180,6 +180,15 @@ android:contentDescription="@string/cheats" android:text="@string/cheats" /> + + + + diff --git a/src/android/app/src/main/res/layout/list_item_setting_switch.xml b/src/android/app/src/main/res/layout/list_item_setting_switch.xml index dc1e82258..d81585119 100644 --- a/src/android/app/src/main/res/layout/list_item_setting_switch.xml +++ b/src/android/app/src/main/res/layout/list_item_setting_switch.xml @@ -46,6 +46,15 @@ android:textAlignment="viewStart" tools:text="@string/frame_limit_enable_description" /> + + diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml index 950ab6fc8..8ca92310f 100644 --- a/src/android/app/src/main/res/menu/menu_in_game.xml +++ b/src/android/app/src/main/res/menu/menu_in_game.xml @@ -57,6 +57,11 @@ android:icon="@drawable/ic_settings" android:title="@string/preferences_settings" /> + + Amiibo Load Remove + Custom Settings + Customized: Revert to Global Select Amiibo File Error Loading Amiibo While loading the specified Amiibo file, an error occurred. Please check that the file is correct. diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 52f406226..e43b58551 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -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; diff --git a/src/common/file_util.h b/src/common/file_util.h index 56dbeea93..cf7b29531 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -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).