android: Add per-game settings

master
t895 2023-12-10 20:38:58 +07:00
parent e975f3cde9
commit 2fce812026
27 changed files with 302 additions and 82 deletions

@ -12,6 +12,7 @@ import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.LongSetting import org.yuzu.yuzu_emu.features.settings.model.LongSetting
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.utils.NativeConfig
/** /**
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
@ -30,9 +31,19 @@ abstract class SettingsItem(
val isEditable: Boolean val isEditable: Boolean
get() { get() {
if (!NativeLibrary.isRunning()) return true if (!NativeLibrary.isRunning()) return true
// Prevent editing settings that were modified in per-game config while editing global
// config
if (!NativeConfig.isPerGameConfigLoaded() && !setting.global) {
return false
}
return setting.isRuntimeModifiable return setting.isRuntimeModifiable
} }
val needsRuntimeGlobal: Boolean
get() = NativeLibrary.isRunning() && !setting.global &&
!NativeConfig.isPerGameConfigLoaded()
companion object { companion object {
const val TYPE_HEADER = 0 const val TYPE_HEADER = 0
const val TYPE_SWITCH = 1 const val TYPE_SWITCH = 1

@ -19,10 +19,9 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navArgs import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.NativeLibrary
import java.io.IOException import java.io.IOException
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
@ -46,6 +45,9 @@ class SettingsActivity : AppCompatActivity() {
binding = ActivitySettingsBinding.inflate(layoutInflater) binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) {
SettingsFile.loadCustomConfig(args.game!!)
}
settingsViewModel.game = args.game settingsViewModel.game = args.game
val navHostFragment = val navHostFragment =
@ -126,7 +128,6 @@ class SettingsActivity : AppCompatActivity() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
// TODO: Load custom settings contextually
if (!DirectoryInitialization.areDirectoriesReady) { if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start() DirectoryInitialization.start()
} }
@ -134,24 +135,35 @@ class SettingsActivity : AppCompatActivity() {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
CoroutineScope(Dispatchers.IO).launch { Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
NativeConfig.saveSettings() if (isFinishing) {
NativeLibrary.applySettings()
if (args.game == null) {
NativeConfig.saveGlobalConfig()
} else if (NativeConfig.isPerGameConfigLoaded()) {
NativeLibrary.logSettings()
NativeConfig.savePerGameConfig()
NativeConfig.unloadPerGameConfig()
}
} }
} }
override fun onDestroy() {
settingsViewModel.clear()
super.onDestroy()
}
fun onSettingsReset() { fun onSettingsReset() {
// Delete settings file because the user may have changed values that do not exist in the UI // Delete settings file because the user may have changed values that do not exist in the UI
NativeConfig.unloadConfig() if (args.game == null) {
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) NativeConfig.unloadGlobalConfig()
if (!settingsFile.delete()) { val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
throw IOException("Failed to delete $settingsFile") if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile")
}
NativeConfig.initializeGlobalConfig()
} else {
NativeConfig.unloadPerGameConfig()
val settingsFile = SettingsFile.getCustomSettingsFile(args.game!!)
if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile")
}
} }
NativeConfig.initializeConfig()
Toast.makeText( Toast.makeText(
applicationContext, applicationContext,

@ -196,6 +196,12 @@ class SettingsAdapter(
return true return true
} }
fun onClearClick(item: SettingsItem, position: Int) {
item.setting.global = true
notifyItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true)
}
private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() { private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
return oldItem.setting.key == newItem.setting.key return oldItem.setting.key == newItem.setting.key

@ -66,7 +66,13 @@ class SettingsFragment : Fragment() {
args.menuTag args.menuTag
) )
binding.toolbarSettingsLayout.title = getString(args.menuTag.titleId) binding.toolbarSettingsLayout.title = if (args.menuTag == Settings.MenuTag.SECTION_ROOT &&
args.game != null
) {
args.game!!.title
} else {
getString(args.menuTag.titleId)
}
binding.listSettings.apply { binding.listSettings.apply {
adapter = settingsAdapter adapter = settingsAdapter
layoutManager = LinearLayoutManager(requireContext()) layoutManager = LinearLayoutManager(requireContext())

@ -7,6 +7,7 @@ import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
@ -31,9 +32,17 @@ class SettingsFragmentPresenter(
private val preferences: SharedPreferences private val preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
// Extension for populating settings list based on paired settings // Extension for altering settings list based on each setting's properties
fun ArrayList<SettingsItem>.add(key: String) { fun ArrayList<SettingsItem>.add(key: String) {
val item = SettingsItem.settingsItems[key]!! val item = SettingsItem.settingsItems[key]!!
if (settingsViewModel.game != null && !item.setting.isSwitchable) {
return
}
if (!NativeConfig.isPerGameConfigLoaded() && !NativeLibrary.isRunning()) {
item.setting.global = true
}
val pairedSettingKey = item.setting.pairedSettingKey val pairedSettingKey = item.setting.pairedSettingKey
if (pairedSettingKey.isNotEmpty()) { if (pairedSettingKey.isNotEmpty()) {
val pairedSettingValue = NativeConfig.getBoolean( val pairedSettingValue = NativeConfig.getBoolean(

@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -35,6 +36,17 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
binding.textSettingValue.text = dateFormatter.format(zonedTime) binding.textSettingValue.text = dateFormatter.format(zonedTime)
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
setStyle(setting.isEditable, binding) setStyle(setting.isEditable, binding)
} }

@ -38,6 +38,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.textSettingValue.visibility = View.GONE binding.textSettingValue.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
setStyle(setting.isEditable, binding) setStyle(setting.isEditable, binding)
} }

@ -41,6 +41,7 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
binding.textSettingName.alpha = opacity binding.textSettingName.alpha = opacity
binding.textSettingDescription.alpha = opacity binding.textSettingDescription.alpha = opacity
binding.textSettingValue.alpha = opacity binding.textSettingValue.alpha = opacity
binding.buttonClear.isEnabled = isEditable
} }
fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) { fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) {
@ -48,5 +49,6 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
val opacity = if (isEditable) 1.0f else 0.5f val opacity = if (isEditable) 1.0f else 0.5f
binding.textSettingName.alpha = opacity binding.textSettingName.alpha = opacity
binding.textSettingDescription.alpha = opacity binding.textSettingDescription.alpha = opacity
binding.buttonClear.isEnabled = isEditable
} }
} }

@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -43,6 +44,17 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
} }
} }
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
setStyle(setting.isEditable, binding) setStyle(setting.isEditable, binding)
} }

@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -30,6 +31,17 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
setting.units setting.units
) )
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
setStyle(setting.isEditable, binding) setStyle(setting.isEditable, binding)
} }

@ -37,6 +37,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.textSettingValue.visibility = View.GONE binding.textSettingValue.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {

@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
@ -29,7 +30,18 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
binding.switchWidget.setOnCheckedChangeListener(null) binding.switchWidget.setOnCheckedChangeListener(null)
binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal) binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
adapter.onBooleanClick(item, binding.switchWidget.isChecked) adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition)
}
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
} }
setStyle(setting.isEditable, binding) setStyle(setting.isEditable, binding)

@ -3,15 +3,27 @@
package org.yuzu.yuzu_emu.features.settings.utils package org.yuzu.yuzu_emu.features.settings.utils
import android.net.Uri
import org.yuzu.yuzu_emu.model.Game
import java.io.* import java.io.*
import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.NativeConfig
/** /**
* Contains static methods for interacting with .ini files in which settings are stored. * Contains static methods for interacting with .ini files in which settings are stored.
*/ */
object SettingsFile { object SettingsFile {
const val FILE_NAME_CONFIG = "config" const val FILE_NAME_CONFIG = "config.ini"
fun getSettingsFile(fileName: String): File = fun getSettingsFile(fileName: String): File =
File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini") File(DirectoryInitialization.userDirectory + "/config/" + fileName)
fun getCustomSettingsFile(game: Game): File =
File(DirectoryInitialization.userDirectory + "/config/custom/" + game.settingsName + ".ini")
fun loadCustomConfig(game: Game) {
val fileName = FileUtil.getFilename(Uri.parse(game.path))
NativeConfig.initializePerGameConfig(game.programId, fileName)
}
} }

@ -52,6 +52,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.EmulationViewModel import org.yuzu.yuzu_emu.model.EmulationViewModel
@ -127,6 +128,16 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
return return
} }
if (args.custom) {
SettingsFile.loadCustomConfig(args.game!!)
NativeConfig.unloadPerGameConfig()
} else {
NativeConfig.reloadGlobalConfig()
}
// Install the selected driver asynchronously as the game starts
driverViewModel.onLaunchGame()
// So this fragment doesn't restart on configuration changes; i.e. rotation. // So this fragment doesn't restart on configuration changes; i.e. rotation.
retainInstance = true retainInstance = true
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
@ -217,6 +228,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
true true
} }
R.id.menu_settings_per_game -> {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
args.game,
Settings.MenuTag.SECTION_ROOT
)
binding.root.findNavController().navigate(action)
true
}
R.id.menu_overlay_controls -> { R.id.menu_overlay_controls -> {
showOverlayOptions() showOverlayOptions()
true true

@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
import org.yuzu.yuzu_emu.model.GameDir import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
class GameFolderPropertiesDialogFragment : DialogFragment() { class GameFolderPropertiesDialogFragment : DialogFragment() {
@ -49,6 +50,11 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
.show() .show()
} }
override fun onStop() {
super.onStop()
NativeConfig.saveGlobalConfig()
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putBoolean(DEEP_SCAN, deepScan) outState.putBoolean(DEEP_SCAN, deepScan)

@ -304,6 +304,11 @@ class SetupFragment : Fragment() {
setInsets() setInsets()
} }
override fun onStop() {
super.onStop()
NativeConfig.saveGlobalConfig()
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
if (_binding != null) { if (_binding != null) {

@ -168,6 +168,7 @@ class GamesViewModel : ViewModel() {
fun onCloseGameFoldersFragment() = fun onCloseGameFoldersFragment() =
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
NativeConfig.saveGlobalConfig()
getGameDirs(true) getGameDirs(true)
} }
} }

@ -68,8 +68,4 @@ class SettingsViewModel : ViewModel() {
fun setAdapterItemChanged(value: Int) { fun setAdapterItemChanged(value: Int) {
_adapterItemChanged.value = value _adapterItemChanged.value = value
} }
fun clear() {
game = null
}
} }

@ -28,12 +28,9 @@ import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.CoroutineScope
import java.io.File import java.io.File
import java.io.FilenameFilter import java.io.FilenameFilter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
@ -258,13 +255,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
super.onResume() super.onResume()
} }
override fun onStop() {
super.onStop()
CoroutineScope(Dispatchers.IO).launch {
NativeConfig.saveSettings()
}
}
override fun onDestroy() { override fun onDestroy() {
EmulationActivity.stopForegroundService(this) EmulationActivity.stopForegroundService(this)
super.onDestroy() super.onDestroy()
@ -677,7 +667,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
} }
// Clear existing user data // Clear existing user data
NativeConfig.unloadConfig() NativeConfig.unloadGlobalConfig()
File(DirectoryInitialization.userDirectory!!).deleteRecursively() File(DirectoryInitialization.userDirectory!!).deleteRecursively()
// Copy archive to internal storage // Copy archive to internal storage
@ -696,7 +686,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
// Reinitialize relevant data // Reinitialize relevant data
NativeLibrary.initializeSystem(true) NativeLibrary.initializeSystem(true)
NativeConfig.initializeConfig() NativeConfig.initializeGlobalConfig()
gamesViewModel.reloadGames(false) gamesViewModel.reloadGames(false)
return@newInstance getString(R.string.user_data_import_success) return@newInstance getString(R.string.user_data_import_success)

@ -16,7 +16,7 @@ object DirectoryInitialization {
if (!areDirectoriesReady) { if (!areDirectoriesReady) {
initializeInternalStorage() initializeInternalStorage()
NativeLibrary.initializeSystem(false) NativeLibrary.initializeSystem(false)
NativeConfig.initializeConfig() NativeConfig.initializeGlobalConfig()
areDirectoriesReady = true areDirectoriesReady = true
} }
} }

@ -7,30 +7,54 @@ import org.yuzu.yuzu_emu.model.GameDir
object NativeConfig { object NativeConfig {
/** /**
* Creates a Config object and opens the emulation config. * Loads global config.
*/ */
@Synchronized @Synchronized
external fun initializeConfig() external fun initializeGlobalConfig()
/** /**
* Destroys the stored config object. This automatically saves the existing config. * Destroys the stored global config object. This does not save the existing config.
*/ */
@Synchronized @Synchronized
external fun unloadConfig() external fun unloadGlobalConfig()
/** /**
* Reads values saved to the config file and saves them. * Reads values in the global config file and saves them.
*/ */
@Synchronized @Synchronized
external fun reloadSettings() external fun reloadGlobalConfig()
/** /**
* Saves settings values in memory to disk. * Saves global settings values in memory to disk.
*/ */
@Synchronized @Synchronized
external fun saveSettings() external fun saveGlobalConfig()
external fun getBoolean(key: String, getDefault: Boolean): Boolean /**
* Creates per-game config for the specified parameters. Must be unloaded once per-game config
* is closed with [unloadPerGameConfig]. All switchable values that [NativeConfig] gets/sets
* will follow the per-game config until the global config is reloaded.
*
* @param programId String representation of the u64 programId
* @param fileName Filename of the game, including its extension
*/
@Synchronized
external fun initializePerGameConfig(programId: String, fileName: String)
@Synchronized
external fun isPerGameConfigLoaded(): Boolean
/**
* Saves per-game settings values in memory to disk.
*/
@Synchronized
external fun savePerGameConfig()
/**
* Destroys the stored per-game config object. This does not save the config.
*/
@Synchronized
external fun unloadPerGameConfig()
@Synchronized @Synchronized
external fun getBoolean(key: String, needsGlobal: Boolean): Boolean external fun getBoolean(key: String, needsGlobal: Boolean): Boolean

@ -3,6 +3,7 @@
#include <string> #include <string>
#include <common/fs/fs_util.h>
#include <jni.h> #include <jni.h>
#include "android_config.h" #include "android_config.h"
@ -12,17 +13,19 @@
#include "frontend_common/config.h" #include "frontend_common/config.h"
#include "jni/android_common/android_common.h" #include "jni/android_common/android_common.h"
#include "jni/id_cache.h" #include "jni/id_cache.h"
#include "native.h"
std::unique_ptr<AndroidConfig> config; std::unique_ptr<AndroidConfig> global_config;
std::unique_ptr<AndroidConfig> per_game_config;
template <typename T> template <typename T>
Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) { Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
auto key = GetJString(env, jkey); auto key = GetJString(env, jkey);
auto basicSetting = Settings::values.linkage.by_key[key]; auto basicSetting = Settings::values.linkage.by_key[key];
auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key];
if (basicSetting != 0) { if (basicSetting != 0) {
return static_cast<Settings::Setting<T>*>(basicSetting); return static_cast<Settings::Setting<T>*>(basicSetting);
} }
auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key];
if (basicAndroidSetting != 0) { if (basicAndroidSetting != 0) {
return static_cast<Settings::Setting<T>*>(basicAndroidSetting); return static_cast<Settings::Setting<T>*>(basicAndroidSetting);
} }
@ -32,20 +35,43 @@ Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
extern "C" { extern "C" {
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeConfig(JNIEnv* env, jobject obj) { void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
config = std::make_unique<AndroidConfig>(); global_config = std::make_unique<AndroidConfig>();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadConfig(JNIEnv* env, jobject obj) { void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
config.reset(); global_config.reset();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadSettings(JNIEnv* env, jobject obj) { void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadGlobalConfig(JNIEnv* env, jobject obj) {
config->AndroidConfig::ReloadAllValues(); global_config->AndroidConfig::ReloadAllValues();
} }
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveSettings(JNIEnv* env, jobject obj) { void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveGlobalConfig(JNIEnv* env, jobject obj) {
config->AndroidConfig::SaveAllValues(); global_config->AndroidConfig::SaveAllValues();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializePerGameConfig(JNIEnv* env, jobject obj,
jstring jprogramId,
jstring jfileName) {
auto program_id = EmulationSession::GetProgramId(env, jprogramId);
auto file_name = GetJString(env, jfileName);
const auto config_file_name = program_id == 0 ? file_name : fmt::format("{:016X}", program_id);
per_game_config =
std::make_unique<AndroidConfig>(config_file_name, Config::ConfigType::PerGameConfig);
}
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_isPerGameConfigLoaded(JNIEnv* env,
jobject obj) {
return per_game_config != nullptr;
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_savePerGameConfig(JNIEnv* env, jobject obj) {
per_game_config->AndroidConfig::SaveAllValues();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadPerGameConfig(JNIEnv* env, jobject obj) {
per_game_config.reset();
} }
jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj, jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj,

@ -62,6 +62,16 @@
android:textSize="13sp" android:textSize="13sp"
tools:text="1x" /> tools:text="1x" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_clear"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone"
android:text="@string/clear"
tools:visibility="visible" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

@ -10,41 +10,62 @@
android:minHeight="72dp" android:minHeight="72dp"
android:padding="16dp"> android:padding="16dp">
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerVertical="true"
android:layout_marginEnd="24dp"
android:layout_toStartOf="@+id/switch_widget"
android:gravity="center_vertical"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView <LinearLayout
android:id="@+id/text_setting_name" android:layout_width="match_parent"
style="@style/TextAppearance.Material3.HeadlineMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAlignment="viewStart" android:orientation="horizontal">
android:textSize="17sp"
app:lineHeight="28dp"
tools:text="@string/frame_limit_enable" />
<com.google.android.material.textview.MaterialTextView <LinearLayout
android:id="@+id/text_setting_description" android:layout_width="0dp"
style="@style/TextAppearance.Material3.BodySmall" android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:gravity="center_vertical"
android:orientation="vertical"
android:layout_weight="1">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_setting_name"
style="@style/TextAppearance.Material3.HeadlineMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textSize="17sp"
app:lineHeight="28dp"
tools:text="@string/frame_limit_enable" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_setting_description"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:textAlignment="viewStart"
tools:text="@string/frame_limit_enable_description" />
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_widget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"/>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_clear"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small" android:layout_marginTop="16dp"
android:textAlignment="viewStart" android:text="@string/clear"
tools:text="@string/frame_limit_enable_description" /> android:visibility="gone"
tools:visibility="visible" />
</LinearLayout> </LinearLayout>

@ -11,6 +11,11 @@
android:icon="@drawable/ic_settings" android:icon="@drawable/ic_settings"
android:title="@string/preferences_settings" /> android:title="@string/preferences_settings" />
<item
android:id="@+id/menu_settings_per_game"
android:icon="@drawable/ic_settings_outline"
android:title="@string/per_game_settings" />
<item <item
android:id="@+id/menu_overlay_controls" android:id="@+id/menu_overlay_controls"
android:icon="@drawable/ic_controller" android:icon="@drawable/ic_controller"

@ -15,6 +15,10 @@
app:argType="org.yuzu.yuzu_emu.model.Game" app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true" app:nullable="true"
android:defaultValue="@null" /> android:defaultValue="@null" />
<argument
android:name="custom"
app:argType="boolean"
android:defaultValue="false" />
</fragment> </fragment>
<activity <activity

@ -77,6 +77,10 @@
app:argType="org.yuzu.yuzu_emu.model.Game" app:argType="org.yuzu.yuzu_emu.model.Game"
app:nullable="true" app:nullable="true"
android:defaultValue="@null" /> android:defaultValue="@null" />
<argument
android:name="custom"
app:argType="boolean"
android:defaultValue="false" />
</activity> </activity>
<action <action