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:
David Griswold 2026-03-15 15:56:26 +03:00
parent 1121a9e6c4
commit a647cabda9
27 changed files with 308 additions and 82 deletions

View File

@ -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)
}

View File

@ -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 {

View File

@ -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(

View File

@ -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()
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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.

View File

@ -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()
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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)
}
}
}
}
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {

View File

@ -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
}

View File

@ -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
}

View File

@ -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();
}

View File

@ -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);
};

View File

@ -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();

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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;

View File

@ -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).