mirror of
https://github.com/izzy2lost/xemu.git
synced 2026-07-06 00:20:22 -07:00
added per game settings
This commit is contained in:
@@ -61,5 +61,10 @@
|
||||
android:screenOrientation="fullSensor"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".PerGameSettingsActivity"
|
||||
android:screenOrientation="fullSensor"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -39,6 +39,7 @@ extern "C" AddfdInfo* monitor_fdset_add_fd(int fd, bool has_fdset_id,
|
||||
namespace {
|
||||
constexpr const char* kLogTag = "xemu-android";
|
||||
constexpr const char* kPrefsName = "x1box_prefs";
|
||||
constexpr const char* kRuntimeOverridePrefPrefix = "runtime_override_";
|
||||
constexpr const char* kDebugLogPrefKey = "setting_debug_logs_enabled";
|
||||
constexpr const char* kHrtfPrefKey = "setting_hrtf";
|
||||
constexpr const char* kHrtfDefaultOffMigrationPrefKey =
|
||||
@@ -56,6 +57,12 @@ static jobject GetActivity(JNIEnv* env);
|
||||
static bool HasException(JNIEnv* env, const char* context);
|
||||
static std::string GetFilesDirPath(JNIEnv* env, jobject activity);
|
||||
static std::string GetPrefString(JNIEnv* env, jobject activity, const char* key);
|
||||
static std::string GetEffectivePrefString(JNIEnv* env, jobject activity,
|
||||
const char* key);
|
||||
static bool GetEffectivePrefBool(JNIEnv* env, jobject activity, const char* key,
|
||||
bool defValue);
|
||||
static int GetEffectivePrefInt(JNIEnv* env, jobject activity, const char* key,
|
||||
int defValue);
|
||||
static void ConfigureNativeDebugLogging(JNIEnv* env, jobject activity);
|
||||
static void ApplyHrtfDefaultOffMigration(JNIEnv* env, jobject activity);
|
||||
static bool NativeDebugLoggingEnabled();
|
||||
@@ -348,7 +355,8 @@ static std::string ResolveAndroidOrientationHint(JNIEnv* env, jobject activity)
|
||||
return kDefaultOrientationHint;
|
||||
}
|
||||
|
||||
std::string value = ToLowerAscii(GetPrefString(env, activity, "setting_game_orientation"));
|
||||
std::string value =
|
||||
ToLowerAscii(GetEffectivePrefString(env, activity, "setting_game_orientation"));
|
||||
if (value == "landscape") {
|
||||
return "LandscapeLeft";
|
||||
}
|
||||
@@ -729,6 +737,74 @@ static int GetPrefInt(JNIEnv* env, jobject activity, const char* key, int defVal
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string BuildRuntimeOverrideKey(const char* key) {
|
||||
if (!key || key[0] == '\0') {
|
||||
return {};
|
||||
}
|
||||
return std::string(kRuntimeOverridePrefPrefix) + key;
|
||||
}
|
||||
|
||||
static bool ParseOverrideBool(const std::string& value, bool* out) {
|
||||
if (!out) {
|
||||
return false;
|
||||
}
|
||||
const std::string normalized = ToLowerAscii(value);
|
||||
if (normalized == "true" || normalized == "1") {
|
||||
*out = true;
|
||||
return true;
|
||||
}
|
||||
if (normalized == "false" || normalized == "0") {
|
||||
*out = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static std::string GetEffectivePrefString(JNIEnv* env, jobject activity,
|
||||
const char* key) {
|
||||
const std::string runtimeKey = BuildRuntimeOverrideKey(key);
|
||||
if (!runtimeKey.empty()) {
|
||||
const std::string overrideValue =
|
||||
GetPrefString(env, activity, runtimeKey.c_str());
|
||||
if (!overrideValue.empty()) {
|
||||
return overrideValue;
|
||||
}
|
||||
}
|
||||
return GetPrefString(env, activity, key);
|
||||
}
|
||||
|
||||
static bool GetEffectivePrefBool(JNIEnv* env, jobject activity, const char* key,
|
||||
bool defValue) {
|
||||
const std::string runtimeKey = BuildRuntimeOverrideKey(key);
|
||||
if (!runtimeKey.empty()) {
|
||||
const std::string overrideValue =
|
||||
GetPrefString(env, activity, runtimeKey.c_str());
|
||||
bool parsed = false;
|
||||
if (ParseOverrideBool(overrideValue, &parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return GetPrefBool(env, activity, key, defValue);
|
||||
}
|
||||
|
||||
static int GetEffectivePrefInt(JNIEnv* env, jobject activity, const char* key,
|
||||
int defValue) {
|
||||
const std::string runtimeKey = BuildRuntimeOverrideKey(key);
|
||||
if (!runtimeKey.empty()) {
|
||||
const std::string overrideValue =
|
||||
GetPrefString(env, activity, runtimeKey.c_str());
|
||||
if (!overrideValue.empty()) {
|
||||
char* end = nullptr;
|
||||
const long parsed = std::strtol(overrideValue.c_str(), &end, 10);
|
||||
if (end != overrideValue.c_str() && end && *end == '\0' &&
|
||||
parsed >= INT_MIN && parsed <= INT_MAX) {
|
||||
return static_cast<int>(parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return GetPrefInt(env, activity, key, defValue);
|
||||
}
|
||||
|
||||
static void ConfigureNativeDebugLogging(JNIEnv* env, jobject activity) {
|
||||
g_native_debug_logging_enabled.store(
|
||||
GetPrefBool(env, activity, kDebugLogPrefKey, false));
|
||||
@@ -1273,29 +1349,36 @@ static SetupFiles SyncSetupFiles() {
|
||||
}
|
||||
|
||||
EmulatorSettings emuSettings;
|
||||
emuSettings.surface_scale = GetPrefInt(env, activity, "setting_surface_scale", 1);
|
||||
emuSettings.surface_scale =
|
||||
GetEffectivePrefInt(env, activity, "setting_surface_scale", 1);
|
||||
emuSettings.system_memory_mib =
|
||||
GetPrefInt(env, activity, "setting_system_memory_mib", 64);
|
||||
GetEffectivePrefInt(env, activity, "setting_system_memory_mib", 64);
|
||||
if (emuSettings.system_memory_mib != 64 &&
|
||||
emuSettings.system_memory_mib != 128) {
|
||||
emuSettings.system_memory_mib = 64;
|
||||
}
|
||||
emuSettings.use_dsp = GetPrefBool(env, activity, "setting_use_dsp", false);
|
||||
emuSettings.hrtf = GetPrefBool(env, activity, kHrtfPrefKey, false);
|
||||
emuSettings.cache_shaders = GetPrefBool(env, activity, "setting_cache_shaders", true);
|
||||
emuSettings.hard_fpu = GetPrefBool(env, activity, "setting_hard_fpu", true);
|
||||
emuSettings.use_dsp =
|
||||
GetEffectivePrefBool(env, activity, "setting_use_dsp", false);
|
||||
emuSettings.hrtf =
|
||||
GetEffectivePrefBool(env, activity, kHrtfPrefKey, false);
|
||||
emuSettings.cache_shaders =
|
||||
GetEffectivePrefBool(env, activity, "setting_cache_shaders", true);
|
||||
emuSettings.hard_fpu =
|
||||
GetEffectivePrefBool(env, activity, "setting_hard_fpu", true);
|
||||
emuSettings.skip_boot_anim =
|
||||
GetPrefBool(env, activity, "setting_skip_boot_anim", false);
|
||||
GetEffectivePrefBool(env, activity, "setting_skip_boot_anim", false);
|
||||
emuSettings.network_enabled =
|
||||
GetPrefBool(env, activity, "setting_network_enable", false);
|
||||
GetEffectivePrefBool(env, activity, "setting_network_enable", false);
|
||||
{
|
||||
std::string tcgThread = GetPrefString(env, activity, "setting_tcg_thread");
|
||||
std::string tcgThread =
|
||||
GetEffectivePrefString(env, activity, "setting_tcg_thread");
|
||||
if (tcgThread == "single") {
|
||||
emuSettings.tcg_thread = "single";
|
||||
}
|
||||
}
|
||||
{
|
||||
std::string rendererPref = GetPrefString(env, activity, "setting_renderer");
|
||||
std::string rendererPref =
|
||||
GetEffectivePrefString(env, activity, "setting_renderer");
|
||||
if (rendererPref == "vulkan") {
|
||||
emuSettings.renderer = "vulkan";
|
||||
} else if (rendererPref == "opengl") {
|
||||
@@ -1303,13 +1386,16 @@ static SetupFiles SyncSetupFiles() {
|
||||
}
|
||||
}
|
||||
{
|
||||
std::string filteringPref = GetPrefString(env, activity, "setting_filtering");
|
||||
std::string filteringPref =
|
||||
GetEffectivePrefString(env, activity, "setting_filtering");
|
||||
if (filteringPref == "nearest") {
|
||||
emuSettings.filtering = "nearest";
|
||||
}
|
||||
}
|
||||
emuSettings.vsync = GetPrefBool(env, activity, "setting_vsync", false);
|
||||
out.audio_driver = GetPrefString(env, activity, "setting_audio_driver");
|
||||
emuSettings.vsync =
|
||||
GetEffectivePrefBool(env, activity, "setting_vsync", false);
|
||||
out.audio_driver =
|
||||
GetEffectivePrefString(env, activity, "setting_audio_driver");
|
||||
{
|
||||
std::string normalized = ToLowerAscii(out.audio_driver);
|
||||
if (normalized == "android" || normalized == "audiotrack") {
|
||||
@@ -1317,7 +1403,7 @@ static SetupFiles SyncSetupFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
int displayMode = GetPrefInt(env, activity, "setting_display_mode", 0);
|
||||
int displayMode = GetEffectivePrefInt(env, activity, "setting_display_mode", 0);
|
||||
xemu_android_set_display_mode_setting(displayMode);
|
||||
|
||||
unsetenv("XEMU_VULKAN_DRIVER");
|
||||
|
||||
@@ -13,9 +13,15 @@ object FrontendLaunchHelper {
|
||||
data class LaunchTarget(
|
||||
val dvdUri: Uri? = null,
|
||||
val dvdPath: String? = null,
|
||||
val relativePath: String? = null,
|
||||
val source: String
|
||||
)
|
||||
|
||||
private data class TreeMatch(
|
||||
val uri: Uri,
|
||||
val relativePath: String,
|
||||
)
|
||||
|
||||
private val stringExtraKeys = listOf(
|
||||
"rom",
|
||||
"ROM",
|
||||
@@ -151,7 +157,11 @@ object FrontendLaunchHelper {
|
||||
|
||||
val treeMatch = resolvePathAgainstGamesFolder(context, gamesFolderUri, path)
|
||||
if (treeMatch != null) {
|
||||
return LaunchTarget(dvdUri = treeMatch, source = label)
|
||||
return LaunchTarget(
|
||||
dvdUri = treeMatch.uri,
|
||||
relativePath = treeMatch.relativePath,
|
||||
source = label,
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
@@ -161,7 +171,7 @@ object FrontendLaunchHelper {
|
||||
context: Context,
|
||||
gamesFolderUri: Uri?,
|
||||
rawPath: String
|
||||
): Uri? {
|
||||
): TreeMatch? {
|
||||
val treeUri = gamesFolderUri ?: return null
|
||||
val treeRootPath = treeUriToFilesystemPath(treeUri) ?: return null
|
||||
val normalizedTree = normalizeFilesystemPath(treeRootPath)
|
||||
@@ -175,7 +185,9 @@ object FrontendLaunchHelper {
|
||||
|
||||
var node = DocumentFile.fromTreeUri(context, treeUri) ?: return null
|
||||
if (relativePath.isEmpty()) {
|
||||
return node.takeIf { it.isFile }?.uri
|
||||
return node.takeIf { it.isFile }?.let { file ->
|
||||
TreeMatch(uri = file.uri, relativePath = "")
|
||||
}
|
||||
}
|
||||
|
||||
for (segment in relativePath.split('/')) {
|
||||
@@ -184,7 +196,9 @@ object FrontendLaunchHelper {
|
||||
}
|
||||
node = node.findFile(segment) ?: return null
|
||||
}
|
||||
return node.takeIf { it.isFile }?.uri
|
||||
return node.takeIf { it.isFile }?.let { file ->
|
||||
TreeMatch(uri = file.uri, relativePath = relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
private fun treeUriToFilesystemPath(treeUri: Uri): String? {
|
||||
|
||||
@@ -65,6 +65,7 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private enum class GameContextAction {
|
||||
PER_GAME_SETTINGS,
|
||||
SET_CUSTOM_COVER,
|
||||
REMOVE_CUSTOM_COVER,
|
||||
DELETE_GAME,
|
||||
@@ -283,7 +284,13 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
|
||||
// MainActivity runs in :xemu, so the disc selection must be flushed before
|
||||
// the other process reads SharedPreferences during startup.
|
||||
prefs.edit()
|
||||
val launchEditor = prefs.edit()
|
||||
PerGameSettingsManager.applyRuntimeOverridesToEditor(
|
||||
context = this,
|
||||
editor = launchEditor,
|
||||
relativePath = null,
|
||||
)
|
||||
launchEditor
|
||||
.remove("dvdUri")
|
||||
.remove("dvdPath")
|
||||
.putBoolean("skip_game_picker", false)
|
||||
@@ -1249,7 +1256,13 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
persistUriPermission(game.uri)
|
||||
// MainActivity runs in :xemu, so the disc selection must be flushed before
|
||||
// the other process reads SharedPreferences during startup.
|
||||
prefs.edit()
|
||||
val launchEditor = prefs.edit()
|
||||
PerGameSettingsManager.applyRuntimeOverridesToEditor(
|
||||
context = this,
|
||||
editor = launchEditor,
|
||||
relativePath = game.relativePath,
|
||||
)
|
||||
launchEditor
|
||||
.putString("dvdUri", game.uri.toString())
|
||||
.remove("dvdPath")
|
||||
.putBoolean("skip_game_picker", false)
|
||||
@@ -1689,6 +1702,7 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
if (customCover.exists()) {
|
||||
customCover.delete()
|
||||
}
|
||||
PerGameSettingsManager.clearOverrides(this, game.relativePath)
|
||||
boxArtCache.remove(normalizeCoverKey(game.title))
|
||||
boxArtMisses.remove(normalizeCoverKey(game.title))
|
||||
|
||||
@@ -1699,6 +1713,7 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
private fun showGameContextMenu(game: GameEntry) {
|
||||
val hasCustomCover = getCustomCoverFile(game).exists()
|
||||
val actions = buildList {
|
||||
add(GameContextAction.PER_GAME_SETTINGS)
|
||||
add(GameContextAction.SET_CUSTOM_COVER)
|
||||
if (hasCustomCover) {
|
||||
add(GameContextAction.REMOVE_CUSTOM_COVER)
|
||||
@@ -1707,6 +1722,7 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
}
|
||||
val options = actions.map { action ->
|
||||
when (action) {
|
||||
GameContextAction.PER_GAME_SETTINGS -> getString(R.string.library_per_game_settings_option)
|
||||
GameContextAction.SET_CUSTOM_COVER -> getString(R.string.library_custom_cover_set_option)
|
||||
GameContextAction.REMOVE_CUSTOM_COVER -> getString(R.string.library_custom_cover_remove_option)
|
||||
GameContextAction.DELETE_GAME -> getString(R.string.library_delete_option)
|
||||
@@ -1730,6 +1746,13 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
setOnClickListener {
|
||||
contextDialog.dismiss()
|
||||
when (actions[i]) {
|
||||
GameContextAction.PER_GAME_SETTINGS -> {
|
||||
startActivity(
|
||||
Intent(this@GameLibraryActivity, PerGameSettingsActivity::class.java)
|
||||
.putExtra(PerGameSettingsActivity.EXTRA_GAME_TITLE, game.title)
|
||||
.putExtra(PerGameSettingsActivity.EXTRA_GAME_RELATIVE_PATH, game.relativePath)
|
||||
)
|
||||
}
|
||||
GameContextAction.SET_CUSTOM_COVER -> {
|
||||
pendingCustomCoverGame = game
|
||||
pickCustomCover.launch("image/*")
|
||||
|
||||
@@ -96,21 +96,24 @@ class LauncherActivity : Activity() {
|
||||
}
|
||||
// MainActivity runs in :xemu, so launch data must be flushed before
|
||||
// handing off to the emulator process.
|
||||
prefs.edit()
|
||||
.putBoolean("skip_game_picker", false)
|
||||
.apply {
|
||||
when {
|
||||
frontendLaunch.dvdUri != null -> {
|
||||
putString("dvdUri", frontendLaunch.dvdUri.toString())
|
||||
remove("dvdPath")
|
||||
}
|
||||
frontendLaunch.dvdPath != null -> {
|
||||
putString("dvdPath", frontendLaunch.dvdPath)
|
||||
remove("dvdUri")
|
||||
}
|
||||
}
|
||||
val launchEditor = prefs.edit()
|
||||
launchEditor.putBoolean("skip_game_picker", false)
|
||||
PerGameSettingsManager.applyRuntimeOverridesToEditor(
|
||||
context = this,
|
||||
editor = launchEditor,
|
||||
relativePath = frontendLaunch.relativePath,
|
||||
)
|
||||
when {
|
||||
frontendLaunch.dvdUri != null -> {
|
||||
launchEditor.putString("dvdUri", frontendLaunch.dvdUri.toString())
|
||||
launchEditor.remove("dvdPath")
|
||||
}
|
||||
.commit()
|
||||
frontendLaunch.dvdPath != null -> {
|
||||
launchEditor.putString("dvdPath", frontendLaunch.dvdPath)
|
||||
launchEditor.remove("dvdUri")
|
||||
}
|
||||
}
|
||||
launchEditor.commit()
|
||||
|
||||
if (hasMcpx && hasFlash && hasHdd) {
|
||||
DebugLog.i(TAG) { "Frontend launch resolved via ${frontendLaunch.source}" }
|
||||
|
||||
@@ -58,7 +58,11 @@ object OrientationPreferences {
|
||||
}
|
||||
|
||||
fun getGameOrientation(context: Context): GameOrientation {
|
||||
return GameOrientation.fromPrefValue(sharedPreferences(context).getString(PREF_GAME_ORIENTATION, null))
|
||||
val prefs = sharedPreferences(context)
|
||||
val runtimeOverride = PerGameSettingsManager.getRuntimeOverride(context, PREF_GAME_ORIENTATION)
|
||||
return GameOrientation.fromPrefValue(
|
||||
runtimeOverride ?: prefs.getString(PREF_GAME_ORIENTATION, null)
|
||||
)
|
||||
}
|
||||
|
||||
fun getUiRequestedOrientation(context: Context): Int {
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
package com.izzy2lost.x1box
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
class PerGameSettingsActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
const val EXTRA_GAME_TITLE = "com.izzy2lost.x1box.extra.GAME_TITLE"
|
||||
const val EXTRA_GAME_RELATIVE_PATH = "com.izzy2lost.x1box.extra.GAME_RELATIVE_PATH"
|
||||
}
|
||||
|
||||
private data class SettingOption(
|
||||
val value: String?,
|
||||
val labelRes: Int,
|
||||
)
|
||||
|
||||
private data class SettingField(
|
||||
val key: String,
|
||||
val inputLayoutId: Int,
|
||||
val dropdownId: Int,
|
||||
val options: List<SettingOption>,
|
||||
)
|
||||
|
||||
private val fieldSelections = linkedMapOf<String, String?>()
|
||||
|
||||
private val fields by lazy {
|
||||
listOf(
|
||||
SettingField(
|
||||
key = "setting_renderer",
|
||||
inputLayoutId = R.id.input_per_game_renderer,
|
||||
dropdownId = R.id.dropdown_per_game_renderer,
|
||||
options = listOf(
|
||||
SettingOption(null, R.string.per_game_settings_use_global),
|
||||
SettingOption("vulkan", R.string.settings_graphics_api_vulkan),
|
||||
SettingOption("opengl", R.string.settings_graphics_api_opengl),
|
||||
),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_filtering",
|
||||
inputLayoutId = R.id.input_per_game_filtering,
|
||||
dropdownId = R.id.dropdown_per_game_filtering,
|
||||
options = listOf(
|
||||
SettingOption(null, R.string.per_game_settings_use_global),
|
||||
SettingOption("linear", R.string.settings_filtering_linear),
|
||||
SettingOption("nearest", R.string.settings_filtering_nearest),
|
||||
),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_vsync",
|
||||
inputLayoutId = R.id.input_per_game_vsync,
|
||||
dropdownId = R.id.dropdown_per_game_vsync,
|
||||
options = booleanOptions(),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_surface_scale",
|
||||
inputLayoutId = R.id.input_per_game_surface_scale,
|
||||
dropdownId = R.id.dropdown_per_game_surface_scale,
|
||||
options = listOf(
|
||||
SettingOption(null, R.string.per_game_settings_use_global),
|
||||
SettingOption("1", R.string.settings_resolution_scale_1x),
|
||||
SettingOption("2", R.string.settings_resolution_scale_2x),
|
||||
SettingOption("3", R.string.settings_resolution_scale_3x),
|
||||
),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_display_mode",
|
||||
inputLayoutId = R.id.input_per_game_display_mode,
|
||||
dropdownId = R.id.dropdown_per_game_display_mode,
|
||||
options = listOf(
|
||||
SettingOption(null, R.string.per_game_settings_use_global),
|
||||
SettingOption("0", R.string.settings_display_mode_stretch),
|
||||
SettingOption("1", R.string.settings_display_mode_4_3),
|
||||
SettingOption("2", R.string.settings_display_mode_16_9),
|
||||
),
|
||||
),
|
||||
SettingField(
|
||||
key = OrientationPreferences.PREF_GAME_ORIENTATION,
|
||||
inputLayoutId = R.id.input_per_game_orientation,
|
||||
dropdownId = R.id.dropdown_per_game_orientation,
|
||||
options = listOf(
|
||||
SettingOption(null, R.string.per_game_settings_use_global),
|
||||
SettingOption(
|
||||
OrientationPreferences.GameOrientation.FOLLOW_DEVICE.prefValue,
|
||||
R.string.settings_orientation_follow_device,
|
||||
),
|
||||
SettingOption(
|
||||
OrientationPreferences.GameOrientation.LANDSCAPE.prefValue,
|
||||
R.string.settings_orientation_landscape,
|
||||
),
|
||||
SettingOption(
|
||||
OrientationPreferences.GameOrientation.REVERSE_LANDSCAPE.prefValue,
|
||||
R.string.settings_orientation_reverse_landscape,
|
||||
),
|
||||
),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_system_memory_mib",
|
||||
inputLayoutId = R.id.input_per_game_system_memory,
|
||||
dropdownId = R.id.dropdown_per_game_system_memory,
|
||||
options = listOf(
|
||||
SettingOption(null, R.string.per_game_settings_use_global),
|
||||
SettingOption("64", R.string.settings_system_memory_64),
|
||||
SettingOption("128", R.string.settings_system_memory_128),
|
||||
),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_tcg_thread",
|
||||
inputLayoutId = R.id.input_per_game_tcg_thread,
|
||||
dropdownId = R.id.dropdown_per_game_tcg_thread,
|
||||
options = listOf(
|
||||
SettingOption(null, R.string.per_game_settings_use_global),
|
||||
SettingOption("multi", R.string.settings_tcg_thread_multi),
|
||||
SettingOption("single", R.string.settings_tcg_thread_single),
|
||||
),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_hard_fpu",
|
||||
inputLayoutId = R.id.input_per_game_hard_fpu,
|
||||
dropdownId = R.id.dropdown_per_game_hard_fpu,
|
||||
options = booleanOptions(),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_cache_shaders",
|
||||
inputLayoutId = R.id.input_per_game_cache_shaders,
|
||||
dropdownId = R.id.dropdown_per_game_cache_shaders,
|
||||
options = booleanOptions(),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_skip_boot_anim",
|
||||
inputLayoutId = R.id.input_per_game_skip_boot_anim,
|
||||
dropdownId = R.id.dropdown_per_game_skip_boot_anim,
|
||||
options = booleanOptions(),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_use_dsp",
|
||||
inputLayoutId = R.id.input_per_game_use_dsp,
|
||||
dropdownId = R.id.dropdown_per_game_use_dsp,
|
||||
options = booleanOptions(),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_hrtf",
|
||||
inputLayoutId = R.id.input_per_game_hrtf,
|
||||
dropdownId = R.id.dropdown_per_game_hrtf,
|
||||
options = booleanOptions(),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_audio_driver",
|
||||
inputLayoutId = R.id.input_per_game_audio_driver,
|
||||
dropdownId = R.id.dropdown_per_game_audio_driver,
|
||||
options = listOf(
|
||||
SettingOption(null, R.string.per_game_settings_use_global),
|
||||
SettingOption("openslES", R.string.settings_audio_driver_opensles),
|
||||
SettingOption("aaudio", R.string.settings_audio_driver_aaudio),
|
||||
SettingOption("dummy", R.string.settings_audio_driver_disabled),
|
||||
),
|
||||
),
|
||||
SettingField(
|
||||
key = "setting_network_enable",
|
||||
inputLayoutId = R.id.input_per_game_network_enable,
|
||||
dropdownId = R.id.dropdown_per_game_network_enable,
|
||||
options = booleanOptions(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private lateinit var gameTitle: String
|
||||
private lateinit var relativePath: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
OrientationLocker(this).enable()
|
||||
setContentView(R.layout.activity_per_game_settings)
|
||||
EdgeToEdgeHelper.enable(this)
|
||||
EdgeToEdgeHelper.applySystemBarPadding(findViewById(R.id.per_game_settings_scroll))
|
||||
|
||||
gameTitle = intent.getStringExtra(EXTRA_GAME_TITLE)?.trim().orEmpty()
|
||||
relativePath = intent.getStringExtra(EXTRA_GAME_RELATIVE_PATH)?.trim().orEmpty()
|
||||
|
||||
if (relativePath.isEmpty()) {
|
||||
Toast.makeText(this, R.string.per_game_settings_missing_game, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
findViewById<TextView>(R.id.tv_per_game_settings_game_title).text =
|
||||
gameTitle.ifEmpty { relativePath.substringAfterLast('/') }
|
||||
findViewById<TextView>(R.id.tv_per_game_settings_game_path).text = relativePath
|
||||
|
||||
val savedOverrides = PerGameSettingsManager.loadOverrides(this, relativePath)
|
||||
bindFields(savedOverrides)
|
||||
|
||||
findViewById<MaterialButton>(R.id.btn_per_game_settings_clear).setOnClickListener {
|
||||
PerGameSettingsManager.clearOverrides(this, relativePath)
|
||||
Toast.makeText(this, R.string.per_game_settings_cleared, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
findViewById<MaterialButton>(R.id.btn_per_game_settings_save).setOnClickListener {
|
||||
PerGameSettingsManager.saveOverrides(this, relativePath, fieldSelections)
|
||||
Toast.makeText(this, R.string.per_game_settings_saved, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindFields(savedOverrides: Map<String, String>) {
|
||||
for (field in fields) {
|
||||
val inputLayout = findViewById<TextInputLayout>(field.inputLayoutId)
|
||||
val dropdown = findViewById<AutoCompleteTextView>(field.dropdownId)
|
||||
val labels = field.options.map { option -> getString(option.labelRes) }
|
||||
|
||||
dropdown.setAdapter(ArrayAdapter(this, android.R.layout.simple_list_item_1, labels))
|
||||
dropdown.setOnItemClickListener { _, _, position, _ ->
|
||||
fieldSelections[field.key] = field.options[position].value
|
||||
}
|
||||
|
||||
val selectedValue = savedOverrides[field.key]
|
||||
fieldSelections[field.key] = selectedValue
|
||||
setFieldSelection(dropdown, field, selectedValue)
|
||||
inputLayout.helperText = getString(
|
||||
R.string.per_game_settings_global_value,
|
||||
describeGlobalValue(field),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setFieldSelection(
|
||||
dropdown: AutoCompleteTextView,
|
||||
field: SettingField,
|
||||
value: String?,
|
||||
) {
|
||||
val option = field.options.firstOrNull { it.value == value } ?: field.options.first()
|
||||
dropdown.setText(getString(option.labelRes), false)
|
||||
}
|
||||
|
||||
private fun describeGlobalValue(field: SettingField): String {
|
||||
val globalValue = readGlobalValue(field.key)
|
||||
val matchingOption = field.options.firstOrNull { option -> option.value == globalValue }
|
||||
?: field.options.first()
|
||||
return getString(matchingOption.labelRes)
|
||||
}
|
||||
|
||||
private fun readGlobalValue(key: String): String {
|
||||
val prefs = getSharedPreferences("x1box_prefs", MODE_PRIVATE)
|
||||
return when (key) {
|
||||
"setting_renderer" -> prefs.getString(key, "opengl") ?: "opengl"
|
||||
"setting_filtering" -> prefs.getString(key, "linear") ?: "linear"
|
||||
"setting_vsync" -> prefs.getBoolean(key, false).toString()
|
||||
"setting_surface_scale" -> prefs.getInt(key, 1).toString()
|
||||
"setting_display_mode" -> prefs.getInt(key, 0).toString()
|
||||
OrientationPreferences.PREF_GAME_ORIENTATION ->
|
||||
prefs.getString(
|
||||
key,
|
||||
OrientationPreferences.GameOrientation.FOLLOW_DEVICE.prefValue,
|
||||
) ?: OrientationPreferences.GameOrientation.FOLLOW_DEVICE.prefValue
|
||||
"setting_system_memory_mib" -> prefs.getInt(key, 64).toString()
|
||||
"setting_tcg_thread" -> prefs.getString(key, "multi") ?: "multi"
|
||||
"setting_use_dsp" -> prefs.getBoolean(key, false).toString()
|
||||
"setting_hrtf" -> prefs.getBoolean(key, false).toString()
|
||||
"setting_cache_shaders" -> prefs.getBoolean(key, true).toString()
|
||||
"setting_hard_fpu" -> prefs.getBoolean(key, true).toString()
|
||||
"setting_skip_boot_anim" -> prefs.getBoolean(key, false).toString()
|
||||
"setting_audio_driver" -> prefs.getString(key, "openslES") ?: "openslES"
|
||||
"setting_network_enable" -> prefs.getBoolean(key, false).toString()
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun booleanOptions(): List<SettingOption> {
|
||||
return listOf(
|
||||
SettingOption(null, R.string.per_game_settings_use_global),
|
||||
SettingOption("true", R.string.per_game_settings_enabled),
|
||||
SettingOption("false", R.string.per_game_settings_disabled),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.izzy2lost.x1box
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
|
||||
object PerGameSettingsManager {
|
||||
private const val APP_PREFS_NAME = "x1box_prefs"
|
||||
private const val STORE_PREFS_NAME = "x1box_per_game_settings"
|
||||
private const val STORE_KEY_PREFIX = "game_override:"
|
||||
private const val RUNTIME_KEY_PREFIX = "runtime_override_"
|
||||
|
||||
val overridablePreferenceKeys = listOf(
|
||||
"setting_renderer",
|
||||
"setting_filtering",
|
||||
"setting_vsync",
|
||||
"setting_surface_scale",
|
||||
"setting_display_mode",
|
||||
OrientationPreferences.PREF_GAME_ORIENTATION,
|
||||
"setting_system_memory_mib",
|
||||
"setting_tcg_thread",
|
||||
"setting_use_dsp",
|
||||
"setting_hrtf",
|
||||
"setting_cache_shaders",
|
||||
"setting_hard_fpu",
|
||||
"setting_skip_boot_anim",
|
||||
"setting_audio_driver",
|
||||
"setting_network_enable",
|
||||
)
|
||||
|
||||
fun hasOverrides(context: Context, relativePath: String): Boolean {
|
||||
val prefs = storePreferences(context)
|
||||
val gameId = gameId(relativePath)
|
||||
return overridablePreferenceKeys.any { key ->
|
||||
prefs.contains(storageKey(gameId, key))
|
||||
}
|
||||
}
|
||||
|
||||
fun loadOverrides(context: Context, relativePath: String): Map<String, String> {
|
||||
val prefs = storePreferences(context)
|
||||
val gameId = gameId(relativePath)
|
||||
return buildMap {
|
||||
for (key in overridablePreferenceKeys) {
|
||||
prefs.getString(storageKey(gameId, key), null)
|
||||
?.takeIf { value -> value.isNotEmpty() }
|
||||
?.let { value -> put(key, value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveOverrides(
|
||||
context: Context,
|
||||
relativePath: String,
|
||||
overrides: Map<String, String?>,
|
||||
) {
|
||||
val prefs = storePreferences(context)
|
||||
val gameId = gameId(relativePath)
|
||||
val editor = prefs.edit()
|
||||
for (key in overridablePreferenceKeys) {
|
||||
val value = overrides[key]
|
||||
if (value.isNullOrEmpty()) {
|
||||
editor.remove(storageKey(gameId, key))
|
||||
} else {
|
||||
editor.putString(storageKey(gameId, key), value)
|
||||
}
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun clearOverrides(context: Context, relativePath: String) {
|
||||
val prefs = storePreferences(context)
|
||||
val gameId = gameId(relativePath)
|
||||
val editor = prefs.edit()
|
||||
for (key in overridablePreferenceKeys) {
|
||||
editor.remove(storageKey(gameId, key))
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun applyRuntimeOverridesToEditor(
|
||||
context: Context,
|
||||
editor: SharedPreferences.Editor,
|
||||
relativePath: String?,
|
||||
) {
|
||||
val overrides = relativePath
|
||||
?.takeIf { path -> path.isNotBlank() }
|
||||
?.let { path -> loadOverrides(context, path) }
|
||||
.orEmpty()
|
||||
|
||||
for (key in overridablePreferenceKeys) {
|
||||
val runtimeKey = runtimeKey(key)
|
||||
val value = overrides[key]
|
||||
if (value.isNullOrEmpty()) {
|
||||
editor.remove(runtimeKey)
|
||||
} else {
|
||||
editor.putString(runtimeKey, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getRuntimeOverride(context: Context, key: String): String? {
|
||||
return appPreferences(context).getString(runtimeKey(key), null)
|
||||
?.takeIf { value -> value.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun runtimeKey(key: String): String = RUNTIME_KEY_PREFIX + key
|
||||
|
||||
private fun gameId(relativePath: String): String {
|
||||
val normalized = relativePath.trim().lowercase(Locale.ROOT)
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val bytes = digest.digest(normalized.toByteArray(Charsets.UTF_8))
|
||||
return bytes.joinToString(separator = "") { byte -> "%02x".format(byte.toInt() and 0xFF) }
|
||||
}
|
||||
|
||||
private fun storageKey(gameId: String, key: String): String =
|
||||
STORE_KEY_PREFIX + gameId + ":" + key
|
||||
|
||||
private fun appPreferences(context: Context): SharedPreferences {
|
||||
return context.applicationContext.getSharedPreferences(APP_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
private fun storePreferences(context: Context): SharedPreferences {
|
||||
return context.applicationContext.getSharedPreferences(STORE_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
@@ -1534,7 +1534,13 @@ class SettingsActivity : AppCompatActivity() {
|
||||
private fun launchInsigniaSetupAssistant(uri: Uri) {
|
||||
persistUriPermission(uri)
|
||||
switchNetworkEnable.isChecked = true
|
||||
prefs.edit()
|
||||
val launchEditor = prefs.edit()
|
||||
PerGameSettingsManager.applyRuntimeOverridesToEditor(
|
||||
context = this,
|
||||
editor = launchEditor,
|
||||
relativePath = null,
|
||||
)
|
||||
launchEditor
|
||||
.putBoolean("setting_network_enable", true)
|
||||
.putString("dvdUri", uri.toString())
|
||||
.remove("dvdPath")
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/setup_wizard_background">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/per_game_settings_scroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:paddingHorizontal="@dimen/top_level_card_outer_padding_horizontal"
|
||||
android:paddingVertical="@dimen/top_level_card_outer_padding_vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="@color/xemu_surface"
|
||||
app:cardCornerRadius="28dp"
|
||||
app:cardElevation="14dp"
|
||||
app:strokeColor="@color/xemu_outline"
|
||||
app:strokeWidth="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/per_game_settings_title"
|
||||
android:textAppearance="@style/TextAppearance.Material3.HeadlineMedium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_per_game_settings_game_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
|
||||
android:textColor="@color/xemu_green_light" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_per_game_settings_game_path"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="@color/xemu_text_muted" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/per_game_settings_intro"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="@color/xemu_text_muted" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@drawable/section_header_background"
|
||||
android:text="@string/settings_section_display"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="@color/xemu_green_light" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_renderer"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_graphics_api">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_renderer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_filtering"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_filtering">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_filtering"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_vsync"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_vsync">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_vsync"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_surface_scale"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_resolution_scale">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_surface_scale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_display_mode"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_display_mode">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_display_mode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_orientation"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:hint="@string/settings_in_game_orientation">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_orientation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/settings_in_game_orientation_hint"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="@color/xemu_text_muted" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@color/xemu_outline_variant" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@drawable/section_header_background"
|
||||
android:text="@string/settings_section_performance"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="@color/xemu_green_light" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_system_memory"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_system_memory">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_system_memory"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_tcg_thread"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_tcg_thread">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_tcg_thread"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_hard_fpu"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_hard_fpu">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_hard_fpu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_cache_shaders"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_cache_shaders">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_cache_shaders"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_skip_boot_anim"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="@string/settings_skip_boot_anim">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_skip_boot_anim"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@color/xemu_outline_variant" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@drawable/section_header_background"
|
||||
android:text="@string/settings_section_audio"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="@color/xemu_green_light" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_use_dsp"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_use_dsp">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_use_dsp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_hrtf"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:hint="@string/settings_hrtf">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_hrtf"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_audio_driver"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="@string/settings_audio_driver">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_audio_driver"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@color/xemu_outline_variant" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@drawable/section_header_background"
|
||||
android:text="@string/settings_section_online"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="@color/xemu_green_light" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_per_game_network_enable"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:hint="@string/settings_online_enable">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/dropdown_per_game_network_enable"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="none" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_per_game_settings_clear"
|
||||
style="@style/Widget.Xemu.Button.Outlined.Pill"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/per_game_settings_clear" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_per_game_settings_save"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/per_game_settings_save"
|
||||
android:textColor="@color/xemu_black" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -345,4 +345,17 @@
|
||||
<string name="library_custom_cover_set">Custom cover saved</string>
|
||||
<string name="library_custom_cover_removed">Custom cover removed</string>
|
||||
<string name="library_custom_cover_failed">Failed to save custom cover</string>
|
||||
<string name="library_per_game_settings_option">Per-Game Settings</string>
|
||||
|
||||
<string name="per_game_settings_title">Per-Game Settings</string>
|
||||
<string name="per_game_settings_intro">Choose only the settings this game should override. Leave a field on Use Global to keep the normal app setting.</string>
|
||||
<string name="per_game_settings_use_global">Use Global</string>
|
||||
<string name="per_game_settings_global_value">Global default: %1$s</string>
|
||||
<string name="per_game_settings_enabled">Enabled</string>
|
||||
<string name="per_game_settings_disabled">Disabled</string>
|
||||
<string name="per_game_settings_save">Save Overrides</string>
|
||||
<string name="per_game_settings_clear">Clear Overrides</string>
|
||||
<string name="per_game_settings_saved">Per-game settings saved</string>
|
||||
<string name="per_game_settings_cleared">Per-game settings cleared</string>
|
||||
<string name="per_game_settings_missing_game">This game could not be resolved.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user