yuzu-emu
/
yuzu-mainline
Archived
1
0
Fork 0

Merge pull request #13034 from t895/map-all-the-inputs

android: Input mapping
This commit is contained in:
liamwhite 2024-02-17 22:22:06 -05:00 committed by GitHub
commit e7146309de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
101 changed files with 5999 additions and 1202 deletions

View File

@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<application <application
android:name="org.yuzu.yuzu_emu.YuzuApplication" android:name="org.yuzu.yuzu_emu.YuzuApplication"

View File

@ -30,34 +30,6 @@ import org.yuzu.yuzu_emu.model.GameVerificationResult
* with the native side of the Yuzu code. * with the native side of the Yuzu code.
*/ */
object NativeLibrary { object NativeLibrary {
/**
* Default controller id for each device
*/
const val Player1Device = 0
const val Player2Device = 1
const val Player3Device = 2
const val Player4Device = 3
const val Player5Device = 4
const val Player6Device = 5
const val Player7Device = 6
const val Player8Device = 7
const val ConsoleDevice = 8
/**
* Controller type for each device
*/
const val ProController = 3
const val Handheld = 4
const val JoyconDual = 5
const val JoyconLeft = 6
const val JoyconRight = 7
const val GameCube = 8
const val Pokeball = 9
const val NES = 10
const val SNES = 11
const val N64 = 12
const val SegaGenesis = 13
@JvmField @JvmField
var sEmulationActivity = WeakReference<EmulationActivity?>(null) var sEmulationActivity = WeakReference<EmulationActivity?>(null)
@ -127,112 +99,6 @@ object NativeLibrary {
FileUtil.getFilename(Uri.parse(path)) FileUtil.getFilename(Uri.parse(path))
} }
/**
* Returns true if pro controller isn't available and handheld is
*/
external fun isHandheldOnly(): Boolean
/**
* Changes controller type for a specific device.
*
* @param Device The input descriptor of the gamepad.
* @param Type The NpadStyleIndex of the gamepad.
*/
external fun setDeviceType(Device: Int, Type: Int): Boolean
/**
* Handles event when a gamepad is connected.
*
* @param Device The input descriptor of the gamepad.
*/
external fun onGamePadConnectEvent(Device: Int): Boolean
/**
* Handles event when a gamepad is disconnected.
*
* @param Device The input descriptor of the gamepad.
*/
external fun onGamePadDisconnectEvent(Device: Int): Boolean
/**
* Handles button press events for a gamepad.
*
* @param Device The input descriptor of the gamepad.
* @param Button Key code identifying which button was pressed.
* @param Action Mask identifying which action is happening (button pressed down, or button released).
* @return If we handled the button press.
*/
external fun onGamePadButtonEvent(Device: Int, Button: Int, Action: Int): Boolean
/**
* Handles joystick movement events.
*
* @param Device The device ID of the gamepad.
* @param Axis The axis ID
* @param x_axis The value of the x-axis represented by the given ID.
* @param y_axis The value of the y-axis represented by the given ID.
*/
external fun onGamePadJoystickEvent(
Device: Int,
Axis: Int,
x_axis: Float,
y_axis: Float
): Boolean
/**
* Handles motion events.
*
* @param delta_timestamp The finger id corresponding to this event
* @param gyro_x,gyro_y,gyro_z The value of the accelerometer sensor.
* @param accel_x,accel_y,accel_z The value of the y-axis
*/
external fun onGamePadMotionEvent(
Device: Int,
delta_timestamp: Long,
gyro_x: Float,
gyro_y: Float,
gyro_z: Float,
accel_x: Float,
accel_y: Float,
accel_z: Float
): Boolean
/**
* Signals and load a nfc tag
*
* @param data Byte array containing all the data from a nfc tag
*/
external fun onReadNfcTag(data: ByteArray?): Boolean
/**
* Removes current loaded nfc tag
*/
external fun onRemoveNfcTag(): Boolean
/**
* Handles touch press events.
*
* @param finger_id The finger id corresponding to this event
* @param x_axis The value of the x-axis.
* @param y_axis The value of the y-axis.
*/
external fun onTouchPressed(finger_id: Int, x_axis: Float, y_axis: Float)
/**
* Handles touch movement.
*
* @param x_axis The value of the instantaneous x-axis.
* @param y_axis The value of the instantaneous y-axis.
*/
external fun onTouchMoved(finger_id: Int, x_axis: Float, y_axis: Float)
/**
* Handles touch release events.
*
* @param finger_id The finger id corresponding to this event
*/
external fun onTouchReleased(finger_id: Int)
external fun setAppDirectory(directory: String) external fun setAppDirectory(directory: String)
/** /**
@ -629,46 +495,4 @@ object NativeLibrary {
* Checks if all necessary keys are present for decryption * Checks if all necessary keys are present for decryption
*/ */
external fun areKeysPresent(): Boolean external fun areKeysPresent(): Boolean
/**
* Button type for use in onTouchEvent
*/
object ButtonType {
const val BUTTON_A = 0
const val BUTTON_B = 1
const val BUTTON_X = 2
const val BUTTON_Y = 3
const val STICK_L = 4
const val STICK_R = 5
const val TRIGGER_L = 6
const val TRIGGER_R = 7
const val TRIGGER_ZL = 8
const val TRIGGER_ZR = 9
const val BUTTON_PLUS = 10
const val BUTTON_MINUS = 11
const val DPAD_LEFT = 12
const val DPAD_UP = 13
const val DPAD_RIGHT = 14
const val DPAD_DOWN = 15
const val BUTTON_SL = 16
const val BUTTON_SR = 17
const val BUTTON_HOME = 18
const val BUTTON_CAPTURE = 19
}
/**
* Stick type for use in onTouchEvent
*/
object StickType {
const val STICK_L = 0
const val STICK_R = 1
}
/**
* Button states
*/
object ButtonState {
const val RELEASED = 0
const val PRESSED = 1
}
} }

View File

@ -7,6 +7,7 @@ import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import org.yuzu.yuzu_emu.features.input.NativeInput
import java.io.File import java.io.File
import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.DocumentsTree import org.yuzu.yuzu_emu.utils.DocumentsTree
@ -37,6 +38,7 @@ class YuzuApplication : Application() {
documentsTree = DocumentsTree() documentsTree = DocumentsTree()
DirectoryInitialization.start() DirectoryInitialization.start()
GpuDriverHelper.initializeDriverParameters() GpuDriverHelper.initializeDriverParameters()
NativeInput.reloadInputDevices()
NativeLibrary.logDeviceInfo() NativeLibrary.logDeviceInfo()
Log.logDeviceInfo() Log.logDeviceInfo()

View File

@ -39,6 +39,7 @@ 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.databinding.ActivityEmulationBinding import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
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
@ -47,7 +48,9 @@ import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.InputHandler import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.MemoryUtil import org.yuzu.yuzu_emu.utils.MemoryUtil
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.NfcReader import org.yuzu.yuzu_emu.utils.NfcReader
import org.yuzu.yuzu_emu.utils.ParamPackage
import org.yuzu.yuzu_emu.utils.ThemeHelper import org.yuzu.yuzu_emu.utils.ThemeHelper
import java.text.NumberFormat import java.text.NumberFormat
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -63,8 +66,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private var motionTimestamp: Long = 0 private var motionTimestamp: Long = 0
private var flipMotionOrientation: Boolean = false private var flipMotionOrientation: Boolean = false
private var controllerIds = InputHandler.getGameControllerIds()
private val actionPause = "ACTION_EMULATOR_PAUSE" private val actionPause = "ACTION_EMULATOR_PAUSE"
private val actionPlay = "ACTION_EMULATOR_PLAY" private val actionPlay = "ACTION_EMULATOR_PLAY"
private val actionMute = "ACTION_EMULATOR_MUTE" private val actionMute = "ACTION_EMULATOR_MUTE"
@ -78,6 +79,27 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
InputHandler.updateControllerData()
val playerOne = NativeConfig.getInputSettings(true)[0]
if (!playerOne.hasMapping() && InputHandler.androidControllers.isNotEmpty()) {
var params: ParamPackage? = null
for (controller in InputHandler.registeredControllers) {
if (controller.get("port", -1) == 0) {
params = controller
break
}
}
if (params != null) {
NativeInput.updateMappingsWithDefault(
0,
params,
params.get("display", getString(R.string.unknown))
)
NativeConfig.saveGlobalConfig()
}
}
binding = ActivityEmulationBinding.inflate(layoutInflater) binding = ActivityEmulationBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@ -95,8 +117,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
nfcReader = NfcReader(this) nfcReader = NfcReader(this)
nfcReader.initialize() nfcReader.initialize()
InputHandler.initialize()
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) { if (!preferences.getBoolean(Settings.PREF_MEMORY_WARNING_SHOWN, false)) {
if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) { if (MemoryUtil.isLessThan(MemoryUtil.REQUIRED_MEMORY, MemoryUtil.totalMemory)) {
@ -147,7 +167,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onResume() super.onResume()
nfcReader.startScanning() nfcReader.startScanning()
startMotionSensorListener() startMotionSensorListener()
InputHandler.updateControllerIds() InputHandler.updateControllerData()
buildPictureInPictureParams() buildPictureInPictureParams()
} }
@ -172,6 +192,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
nfcReader.onNewIntent(intent) nfcReader.onNewIntent(intent)
InputHandler.updateControllerData()
} }
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@ -244,8 +265,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
} }
val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000 val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000
motionTimestamp = event.timestamp motionTimestamp = event.timestamp
NativeLibrary.onGamePadMotionEvent( NativeInput.onDeviceMotionEvent(
NativeLibrary.Player1Device, NativeInput.Player1Device,
deltaTimestamp, deltaTimestamp,
gyro[0], gyro[0],
gyro[1], gyro[1],
@ -254,8 +275,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
accel[1], accel[1],
accel[2] accel[2]
) )
NativeLibrary.onGamePadMotionEvent( NativeInput.onDeviceMotionEvent(
NativeLibrary.ConsoleDevice, NativeInput.ConsoleDevice,
deltaTimestamp, deltaTimestamp,
gyro[0], gyro[0],
gyro[1], gyro[1],

View File

@ -0,0 +1,416 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input
import org.yuzu.yuzu_emu.features.input.model.NativeButton
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.features.input.model.InputType
import org.yuzu.yuzu_emu.features.input.model.ButtonName
import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ParamPackage
import android.view.InputDevice
object NativeInput {
/**
* Default controller id for each device
*/
const val Player1Device = 0
const val Player2Device = 1
const val Player3Device = 2
const val Player4Device = 3
const val Player5Device = 4
const val Player6Device = 5
const val Player7Device = 6
const val Player8Device = 7
const val ConsoleDevice = 8
/**
* Button states
*/
object ButtonState {
const val RELEASED = 0
const val PRESSED = 1
}
/**
* Returns true if pro controller isn't available and handheld is.
* Intended to check where the input overlay should direct its inputs.
*/
external fun isHandheldOnly(): Boolean
/**
* Handles button press events for a gamepad.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param buttonId The Android Keycode corresponding to this event.
* @param action Mask identifying which action is happening (button pressed down, or button released).
*/
external fun onGamePadButtonEvent(
guid: String,
port: Int,
buttonId: Int,
action: Int
)
/**
* Handles axis movement events.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param axis The axis ID.
* @param value Value along the given axis.
*/
external fun onGamePadAxisEvent(guid: String, port: Int, axis: Int, value: Float)
/**
* Handles motion events.
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param port Port determined by controller connection order.
* @param deltaTimestamp The finger id corresponding to this event.
* @param xGyro The value of the x-axis for the gyroscope.
* @param yGyro The value of the y-axis for the gyroscope.
* @param zGyro The value of the z-axis for the gyroscope.
* @param xAccel The value of the x-axis for the accelerometer.
* @param yAccel The value of the y-axis for the accelerometer.
* @param zAccel The value of the z-axis for the accelerometer.
*/
external fun onGamePadMotionEvent(
guid: String,
port: Int,
deltaTimestamp: Long,
xGyro: Float,
yGyro: Float,
zGyro: Float,
xAccel: Float,
yAccel: Float,
zAccel: Float
)
/**
* Signals and load a nfc tag
* @param data Byte array containing all the data from a nfc tag.
*/
external fun onReadNfcTag(data: ByteArray?)
/**
* Removes current loaded nfc tag.
*/
external fun onRemoveNfcTag()
/**
* Handles touch press events.
* @param fingerId The finger id corresponding to this event.
* @param xAxis The value of the x-axis on the touchscreen.
* @param yAxis The value of the y-axis on the touchscreen.
*/
external fun onTouchPressed(fingerId: Int, xAxis: Float, yAxis: Float)
/**
* Handles touch movement.
* @param fingerId The finger id corresponding to this event.
* @param xAxis The value of the x-axis on the touchscreen.
* @param yAxis The value of the y-axis on the touchscreen.
*/
external fun onTouchMoved(fingerId: Int, xAxis: Float, yAxis: Float)
/**
* Handles touch release events.
* @param fingerId The finger id corresponding to this event
*/
external fun onTouchReleased(fingerId: Int)
/**
* Sends a button input to the global virtual controllers.
* @param port Port determined by controller connection order.
* @param button The [NativeButton] corresponding to this event.
* @param action Mask identifying which action is happening (button pressed down, or button released).
*/
fun onOverlayButtonEvent(port: Int, button: NativeButton, action: Int) =
onOverlayButtonEventImpl(port, button.int, action)
private external fun onOverlayButtonEventImpl(port: Int, buttonId: Int, action: Int)
/**
* Sends a joystick input to the global virtual controllers.
* @param port Port determined by controller connection order.
* @param stick The [NativeAnalog] corresponding to this event.
* @param xAxis Value along the X axis.
* @param yAxis Value along the Y axis.
*/
fun onOverlayJoystickEvent(port: Int, stick: NativeAnalog, xAxis: Float, yAxis: Float) =
onOverlayJoystickEventImpl(port, stick.int, xAxis, yAxis)
private external fun onOverlayJoystickEventImpl(
port: Int,
stickId: Int,
xAxis: Float,
yAxis: Float
)
/**
* Handles motion events for the global virtual controllers.
* @param port Port determined by controller connection order
* @param deltaTimestamp The finger id corresponding to this event.
* @param xGyro The value of the x-axis for the gyroscope.
* @param yGyro The value of the y-axis for the gyroscope.
* @param zGyro The value of the z-axis for the gyroscope.
* @param xAccel The value of the x-axis for the accelerometer.
* @param yAccel The value of the y-axis for the accelerometer.
* @param zAccel The value of the z-axis for the accelerometer.
*/
external fun onDeviceMotionEvent(
port: Int,
deltaTimestamp: Long,
xGyro: Float,
yGyro: Float,
zGyro: Float,
xAccel: Float,
yAccel: Float,
zAccel: Float
)
/**
* Reloads all input devices from the currently loaded Settings::values.players into HID Core
*/
external fun reloadInputDevices()
/**
* Registers a controller to be used with mapping
* @param device An [InputDevice] or the input overlay wrapped with [YuzuInputDevice]
*/
external fun registerController(device: YuzuInputDevice)
/**
* Gets the names of input devices that have been registered with the input subsystem via [registerController]
*/
external fun getInputDevices(): Array<String>
/**
* Reads all input profiles from disk. Must be called before creating a profile picker.
*/
external fun loadInputProfiles()
/**
* Gets the names of each available input profile.
*/
external fun getInputProfileNames(): Array<String>
/**
* Checks if the user-provided name for an input profile is valid.
* @param name User-provided name for an input profile.
* @return Whether [name] is valid or not.
*/
external fun isProfileNameValid(name: String): Boolean
/**
* Creates a new input profile.
* @param name The new profile's name.
* @param playerIndex Index of the player that's currently being edited. Used to write the profile
* name to this player's config.
* @return Whether creating the profile was successful or not.
*/
external fun createProfile(name: String, playerIndex: Int): Boolean
/**
* Deletes an input profile.
* @param name Name of the profile to delete.
* @param playerIndex Index of the player that's currently being edited. Used to remove the profile
* name from this player's config if they have it loaded.
* @return Whether deleting this profile was successful or not.
*/
external fun deleteProfile(name: String, playerIndex: Int): Boolean
/**
* Loads an input profile.
* @param name Name of the input profile to load.
* @param playerIndex Index of the player that will have this profile loaded.
* @return Whether loading this profile was successful or not.
*/
external fun loadProfile(name: String, playerIndex: Int): Boolean
/**
* Saves an input profile.
* @param name Name of the profile to save.
* @param playerIndex Index of the player that's currently being edited. Used to write the profile
* name to this player's config.
* @return Whether saving the profile was successful or not.
*/
external fun saveProfile(name: String, playerIndex: Int): Boolean
/**
* Intended to be used immediately before a call to [NativeConfig.saveControlPlayerValues]
* Must be used while per-game config is loaded.
*/
external fun loadPerGameConfiguration(
playerIndex: Int,
selectedIndex: Int,
selectedProfileName: String
)
/**
* Tells the input subsystem to start listening for inputs to map.
* @param type Type of input to map as shown by the int property in each [InputType].
*/
external fun beginMapping(type: Int)
/**
* Gets an input's [ParamPackage] as a serialized string. Used for input verification before mapping.
* Must be run after [beginMapping] and before [stopMapping].
*/
external fun getNextInput(): String
/**
* Tells the input subsystem to stop listening for inputs to map.
*/
external fun stopMapping()
/**
* Updates a controller's mappings with auto-mapping params.
* @param playerIndex Index of the player to auto-map.
* @param deviceParams [ParamPackage] representing the device to auto-map as received
* from [getInputDevices].
* @param displayName Name of the device to auto-map as received from the "display" param in [deviceParams].
* Intended to be a way to provide a default name for a controller if the "display" param is empty.
*/
fun updateMappingsWithDefault(
playerIndex: Int,
deviceParams: ParamPackage,
displayName: String
) = updateMappingsWithDefaultImpl(playerIndex, deviceParams.serialize(), displayName)
private external fun updateMappingsWithDefaultImpl(
playerIndex: Int,
deviceParams: String,
displayName: String
)
/**
* Gets the params for a specific button.
* @param playerIndex Index of the player to get params from.
* @param button The [NativeButton] to get params for.
* @return A [ParamPackage] representing a player's specific button.
*/
fun getButtonParam(playerIndex: Int, button: NativeButton): ParamPackage =
ParamPackage(getButtonParamImpl(playerIndex, button.int))
private external fun getButtonParamImpl(playerIndex: Int, buttonId: Int): String
/**
* Sets the params for a specific button.
* @param playerIndex Index of the player to set params for.
* @param button The [NativeButton] to set params for.
* @param param A [ParamPackage] to set.
*/
fun setButtonParam(playerIndex: Int, button: NativeButton, param: ParamPackage) =
setButtonParamImpl(playerIndex, button.int, param.serialize())
private external fun setButtonParamImpl(playerIndex: Int, buttonId: Int, param: String)
/**
* Gets the params for a specific stick.
* @param playerIndex Index of the player to get params from.
* @param stick The [NativeAnalog] to get params for.
* @return A [ParamPackage] representing a player's specific stick.
*/
fun getStickParam(playerIndex: Int, stick: NativeAnalog): ParamPackage =
ParamPackage(getStickParamImpl(playerIndex, stick.int))
private external fun getStickParamImpl(playerIndex: Int, stickId: Int): String
/**
* Sets the params for a specific stick.
* @param playerIndex Index of the player to set params for.
* @param stick The [NativeAnalog] to set params for.
* @param param A [ParamPackage] to set.
*/
fun setStickParam(playerIndex: Int, stick: NativeAnalog, param: ParamPackage) =
setStickParamImpl(playerIndex, stick.int, param.serialize())
private external fun setStickParamImpl(playerIndex: Int, stickId: Int, param: String)
/**
* Gets the int representation of a [ButtonName]. Tells you what to show as the mapped input for
* a button/analog/other.
* @param param A [ParamPackage] that represents a specific button's params.
* @return The [ButtonName] for [param].
*/
fun getButtonName(param: ParamPackage): ButtonName =
ButtonName.from(getButtonNameImpl(param.serialize()))
private external fun getButtonNameImpl(param: String): Int
/**
* Gets each supported [NpadStyleIndex] for a given player.
* @param playerIndex Index of the player to get supported indexes for.
* @return List of each supported [NpadStyleIndex].
*/
fun getSupportedStyleTags(playerIndex: Int): List<NpadStyleIndex> =
getSupportedStyleTagsImpl(playerIndex).map { NpadStyleIndex.from(it) }
private external fun getSupportedStyleTagsImpl(playerIndex: Int): IntArray
/**
* Gets the [NpadStyleIndex] for a given player.
* @param playerIndex Index of the player to get an [NpadStyleIndex] from.
* @return The [NpadStyleIndex] for a given player.
*/
fun getStyleIndex(playerIndex: Int): NpadStyleIndex =
NpadStyleIndex.from(getStyleIndexImpl(playerIndex))
private external fun getStyleIndexImpl(playerIndex: Int): Int
/**
* Sets the [NpadStyleIndex] for a given player.
* @param playerIndex Index of the player to change.
* @param style The new style to set.
*/
fun setStyleIndex(playerIndex: Int, style: NpadStyleIndex) =
setStyleIndexImpl(playerIndex, style.int)
private external fun setStyleIndexImpl(playerIndex: Int, styleIndex: Int)
/**
* Checks if a device is a controller.
* @param params [ParamPackage] for an input device retrieved from [getInputDevices]
* @return Whether the device is a controller or not.
*/
fun isController(params: ParamPackage): Boolean = isControllerImpl(params.serialize())
private external fun isControllerImpl(params: String): Boolean
/**
* Checks if a controller is connected
* @param playerIndex Index of the player to check.
* @return Whether the player is connected or not.
*/
external fun getIsConnected(playerIndex: Int): Boolean
/**
* Connects/disconnects a controller and ensures that connection order stays in-tact.
* @param playerIndex Index of the player to connect/disconnect.
* @param connected Whether to connect or disconnect this controller.
*/
fun connectControllers(playerIndex: Int, connected: Boolean = true) {
val connectedControllers = mutableListOf<Boolean>().apply {
if (connected) {
for (i in 0 until 8) {
add(i <= playerIndex)
}
} else {
for (i in 0 until 8) {
add(i < playerIndex)
}
}
}
connectControllersImpl(connectedControllers.toBooleanArray())
}
private external fun connectControllersImpl(connected: BooleanArray)
/**
* Resets all of the button and analog mappings for a player.
* @param playerIndex Index of the player that will have its mappings reset.
*/
external fun resetControllerMappings(playerIndex: Int)
}

View File

@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input
import android.view.InputDevice
import androidx.annotation.Keep
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.utils.InputHandler.getGUID
@Keep
interface YuzuInputDevice {
fun getName(): String
fun getGUID(): String
fun getPort(): Int
fun getSupportsVibration(): Boolean
fun vibrate(intensity: Float)
fun getAxes(): Array<Int> = arrayOf()
fun hasKeys(keys: IntArray): BooleanArray = BooleanArray(0)
}
class YuzuPhysicalDevice(
private val device: InputDevice,
private val port: Int,
useSystemVibrator: Boolean
) : YuzuInputDevice {
private val vibrator = if (useSystemVibrator) {
YuzuVibrator.getSystemVibrator()
} else {
YuzuVibrator.getControllerVibrator(device)
}
override fun getName(): String {
return device.name
}
override fun getGUID(): String {
return device.getGUID()
}
override fun getPort(): Int {
return port
}
override fun getSupportsVibration(): Boolean {
return vibrator.supportsVibration()
}
override fun vibrate(intensity: Float) {
vibrator.vibrate(intensity)
}
override fun getAxes(): Array<Int> = device.motionRanges.map { it.axis }.toTypedArray()
override fun hasKeys(keys: IntArray): BooleanArray = device.hasKeys(*keys)
}
class YuzuInputOverlayDevice(
private val vibration: Boolean,
private val port: Int
) : YuzuInputDevice {
private val vibrator = YuzuVibrator.getSystemVibrator()
override fun getName(): String {
return YuzuApplication.appContext.getString(R.string.input_overlay)
}
override fun getGUID(): String {
return "00000000000000000000000000000000"
}
override fun getPort(): Int {
return port
}
override fun getSupportsVibration(): Boolean {
if (vibration) {
return vibrator.supportsVibration()
}
return false
}
override fun vibrate(intensity: Float) {
if (vibration) {
vibrator.vibrate(intensity)
}
}
}

View File

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input
import android.content.Context
import android.os.Build
import android.os.CombinedVibration
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.view.InputDevice
import androidx.annotation.Keep
import androidx.annotation.RequiresApi
import org.yuzu.yuzu_emu.YuzuApplication
@Keep
@Suppress("DEPRECATION")
interface YuzuVibrator {
fun supportsVibration(): Boolean
fun vibrate(intensity: Float)
companion object {
fun getControllerVibrator(device: InputDevice): YuzuVibrator =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
YuzuVibratorManager(device.vibratorManager)
} else {
YuzuVibratorManagerCompat(device.vibrator)
}
fun getSystemVibrator(): YuzuVibrator =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = YuzuApplication.appContext
.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
YuzuVibratorManager(vibratorManager)
} else {
val vibrator = YuzuApplication.appContext
.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
YuzuVibratorManagerCompat(vibrator)
}
fun getVibrationEffect(intensity: Float): VibrationEffect? {
if (intensity > 0f) {
return VibrationEffect.createOneShot(
50,
(255.0 * intensity).toInt().coerceIn(1, 255)
)
}
return null
}
}
}
@RequiresApi(Build.VERSION_CODES.S)
class YuzuVibratorManager(private val vibratorManager: VibratorManager) : YuzuVibrator {
override fun supportsVibration(): Boolean {
return vibratorManager.vibratorIds.isNotEmpty()
}
override fun vibrate(intensity: Float) {
val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return
vibratorManager.vibrate(CombinedVibration.createParallel(vibration))
}
}
class YuzuVibratorManagerCompat(private val vibrator: Vibrator) : YuzuVibrator {
override fun supportsVibration(): Boolean {
return vibrator.hasVibrator()
}
override fun vibrate(intensity: Float) {
val vibration = YuzuVibrator.getVibrationEffect(intensity) ?: return
vibrator.vibrate(vibration)
}
}

View File

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
enum class AnalogDirection(val int: Int, val param: String) {
Up(0, "up"),
Down(1, "down"),
Left(2, "left"),
Right(3, "right")
}

View File

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
// Loosely matches the enum in common/input.h
enum class ButtonName(val int: Int) {
Invalid(1),
// This will display the engine name instead of the button name
Engine(2),
// This will display the button by value instead of the button name
Value(3);
companion object {
fun from(int: Int): ButtonName = entries.firstOrNull { it.int == int } ?: Invalid
}
}

View File

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
// Must match the corresponding enum in input_common/main.h
enum class InputType(val int: Int) {
None(0),
Button(1),
Stick(2),
Motion(3),
Touch(4)
}

View File

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
// Must match enum in src/common/settings_input.h
enum class NativeAnalog(val int: Int) {
LStick(0),
RStick(1);
companion object {
fun from(int: Int): NativeAnalog = entries.firstOrNull { it.int == int } ?: LStick
}
}

View File

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
// Must match enum in src/common/settings_input.h
enum class NativeButton(val int: Int) {
A(0),
B(1),
X(2),
Y(3),
LStick(4),
RStick(5),
L(6),
R(7),
ZL(8),
ZR(9),
Plus(10),
Minus(11),
DLeft(12),
DUp(13),
DRight(14),
DDown(15),
SLLeft(16),
SRLeft(17),
Home(18),
Capture(19),
SLRight(20),
SRRight(21);
companion object {
fun from(int: Int): NativeButton = entries.firstOrNull { it.int == int } ?: A
}
}

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
// Must match enum in src/common/settings_input.h
enum class NativeTrigger(val int: Int) {
LTrigger(0),
RTrigger(1)
}

View File

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.R
// Must match enum in src/core/hid/hid_types.h
enum class NpadStyleIndex(val int: Int, @StringRes val nameId: Int = 0) {
None(0),
Fullkey(3, R.string.pro_controller),
Handheld(4, R.string.handheld),
HandheldNES(4),
JoyconDual(5, R.string.dual_joycons),
JoyconLeft(6, R.string.left_joycon),
JoyconRight(7, R.string.right_joycon),
GameCube(8, R.string.gamecube_controller),
Pokeball(9),
NES(10),
SNES(12),
N64(13),
SegaGenesis(14),
SystemExt(32),
System(33);
companion object {
fun from(int: Int): NpadStyleIndex = entries.firstOrNull { it.int == int } ?: None
}
}

View File

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.input.model
import androidx.annotation.Keep
@Keep
data class PlayerInput(
var connected: Boolean,
var buttons: Array<String>,
var analogs: Array<String>,
var motions: Array<String>,
var vibrationEnabled: Boolean,
var vibrationStrength: Int,
var bodyColorLeft: Long,
var bodyColorRight: Long,
var buttonColorLeft: Long,
var buttonColorRight: Long,
var profileName: String,
var useSystemVibrator: Boolean
) {
// It's recommended to use the generated equals() and hashCode() methods
// when using arrays in a data class
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PlayerInput
if (connected != other.connected) return false
if (!buttons.contentEquals(other.buttons)) return false
if (!analogs.contentEquals(other.analogs)) return false
if (!motions.contentEquals(other.motions)) return false
if (vibrationEnabled != other.vibrationEnabled) return false
if (vibrationStrength != other.vibrationStrength) return false
if (bodyColorLeft != other.bodyColorLeft) return false
if (bodyColorRight != other.bodyColorRight) return false
if (buttonColorLeft != other.buttonColorLeft) return false
if (buttonColorRight != other.buttonColorRight) return false
if (profileName != other.profileName) return false
return useSystemVibrator == other.useSystemVibrator
}
override fun hashCode(): Int {
var result = connected.hashCode()
result = 31 * result + buttons.contentHashCode()
result = 31 * result + analogs.contentHashCode()
result = 31 * result + motions.contentHashCode()
result = 31 * result + vibrationEnabled.hashCode()
result = 31 * result + vibrationStrength
result = 31 * result + bodyColorLeft.hashCode()
result = 31 * result + bodyColorRight.hashCode()
result = 31 * result + buttonColorLeft.hashCode()
result = 31 * result + buttonColorRight.hashCode()
result = 31 * result + profileName.hashCode()
result = 31 * result + useSystemVibrator.hashCode()
return result
}
fun hasMapping(): Boolean {
var hasMapping = false
buttons.forEach {
if (it != "[empty]") {
hasMapping = true
}
}
analogs.forEach {
if (it != "[empty]") {
hasMapping = true
}
}
motions.forEach {
if (it != "[empty]") {
hasMapping = true
}
}
return hasMapping
}
}

View File

@ -4,17 +4,30 @@
package org.yuzu.yuzu_emu.features.settings.model package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
object Settings { object Settings {
enum class MenuTag(val titleId: Int) { enum class MenuTag(val titleId: Int = 0) {
SECTION_ROOT(R.string.advanced_settings), SECTION_ROOT(R.string.advanced_settings),
SECTION_SYSTEM(R.string.preferences_system), SECTION_SYSTEM(R.string.preferences_system),
SECTION_RENDERER(R.string.preferences_graphics), SECTION_RENDERER(R.string.preferences_graphics),
SECTION_AUDIO(R.string.preferences_audio), SECTION_AUDIO(R.string.preferences_audio),
SECTION_INPUT(R.string.preferences_controls),
SECTION_INPUT_PLAYER_ONE,
SECTION_INPUT_PLAYER_TWO,
SECTION_INPUT_PLAYER_THREE,
SECTION_INPUT_PLAYER_FOUR,
SECTION_INPUT_PLAYER_FIVE,
SECTION_INPUT_PLAYER_SIX,
SECTION_INPUT_PLAYER_SEVEN,
SECTION_INPUT_PLAYER_EIGHT,
SECTION_THEME(R.string.preferences_theme), SECTION_THEME(R.string.preferences_theme),
SECTION_DEBUG(R.string.preferences_debug); SECTION_DEBUG(R.string.preferences_debug);
} }
fun getPlayerString(player: Int): String =
YuzuApplication.appContext.getString(R.string.preferences_player, player)
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch" const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"

View File

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
import org.yuzu.yuzu_emu.features.input.model.InputType
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.utils.ParamPackage
class AnalogInputSetting(
override val playerIndex: Int,
val nativeAnalog: NativeAnalog,
val analogDirection: AnalogDirection,
@StringRes titleId: Int = 0,
titleString: String = ""
) : InputSetting(titleId, titleString) {
override val type = TYPE_INPUT
override val inputType = InputType.Stick
override fun getSelectedValue(): String {
val params = NativeInput.getStickParam(playerIndex, nativeAnalog)
val analog = analogToText(params, analogDirection.param)
return getDisplayString(params, analog)
}
override fun setSelectedValue(param: ParamPackage) =
NativeInput.setStickParam(playerIndex, nativeAnalog, param)
}

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.utils.ParamPackage
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.InputType
import org.yuzu.yuzu_emu.features.input.model.NativeButton
class ButtonInputSetting(
override val playerIndex: Int,
val nativeButton: NativeButton,
@StringRes titleId: Int = 0,
titleString: String = ""
) : InputSetting(titleId, titleString) {
override val type = TYPE_INPUT
override val inputType = InputType.Button
override fun getSelectedValue(): String {
val params = NativeInput.getButtonParam(playerIndex, nativeButton)
val button = buttonToText(params)
return getDisplayString(params, button)
}
override fun setSelectedValue(param: ParamPackage) =
NativeInput.setButtonParam(playerIndex, nativeButton, param)
}

View File

@ -3,13 +3,16 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractLongSetting
class DateTimeSetting( class DateTimeSetting(
private val longSetting: AbstractLongSetting, private val longSetting: AbstractLongSetting,
titleId: Int, @StringRes titleId: Int = 0,
descriptionId: Int titleString: String = "",
) : SettingsItem(longSetting, titleId, descriptionId) { @StringRes descriptionId: Int = 0,
descriptionString: String = ""
) : SettingsItem(longSetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_DATETIME_SETTING override val type = TYPE_DATETIME_SETTING
fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal) fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)

View File

@ -3,8 +3,11 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
class HeaderSetting( class HeaderSetting(
titleId: Int @StringRes titleId: Int = 0,
) : SettingsItem(emptySetting, titleId, 0) { titleString: String = ""
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
override val type = TYPE_HEADER override val type = TYPE_HEADER
} }

View File

@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.utils.NativeConfig
class InputProfileSetting(private val playerIndex: Int) :
SettingsItem(emptySetting, R.string.profile, "", 0, "") {
override val type = TYPE_INPUT_PROFILE
fun getCurrentProfile(): String =
NativeConfig.getInputSettings(true)[playerIndex].profileName
fun getProfileNames(): Array<String> = NativeInput.getInputProfileNames()
fun isProfileNameValid(name: String): Boolean = NativeInput.isProfileNameValid(name)
fun createProfile(name: String): Boolean = NativeInput.createProfile(name, playerIndex)
fun deleteProfile(name: String): Boolean = NativeInput.deleteProfile(name, playerIndex)
fun loadProfile(name: String): Boolean {
val result = NativeInput.loadProfile(name, playerIndex)
NativeInput.reloadInputDevices()
return result
}
fun saveProfile(name: String): Boolean = NativeInput.saveProfile(name, playerIndex)
}

View File

@ -0,0 +1,134 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.ButtonName
import org.yuzu.yuzu_emu.features.input.model.InputType
import org.yuzu.yuzu_emu.utils.ParamPackage
sealed class InputSetting(
@StringRes titleId: Int,
titleString: String
) : SettingsItem(emptySetting, titleId, titleString, 0, "") {
override val type = TYPE_INPUT
abstract val inputType: InputType
abstract val playerIndex: Int
protected val context get() = YuzuApplication.appContext
abstract fun getSelectedValue(): String
abstract fun setSelectedValue(param: ParamPackage)
protected fun getDisplayString(params: ParamPackage, control: String): String {
val deviceName = params.get("display", "")
deviceName.ifEmpty {
return context.getString(R.string.not_set)
}
return "$deviceName: $control"
}
private fun getDirectionName(direction: String): String =
when (direction) {
"up" -> context.getString(R.string.up)
"down" -> context.getString(R.string.down)
"left" -> context.getString(R.string.left)
"right" -> context.getString(R.string.right)
else -> direction
}
protected fun buttonToText(param: ParamPackage): String {
if (!param.has("engine")) {
return context.getString(R.string.not_set)
}
val toggle = if (param.get("toggle", false)) "~" else ""
val inverted = if (param.get("inverted", false)) "!" else ""
val invert = if (param.get("invert", "+") == "-") "-" else ""
val turbo = if (param.get("turbo", false)) "$" else ""
val commonButtonName = NativeInput.getButtonName(param)
if (commonButtonName == ButtonName.Invalid) {
return context.getString(R.string.invalid)
}
if (commonButtonName == ButtonName.Engine) {
return param.get("engine", "")
}
if (commonButtonName == ButtonName.Value) {
if (param.has("hat")) {
val hat = getDirectionName(param.get("direction", ""))
return context.getString(R.string.qualified_hat, turbo, toggle, inverted, hat)
}
if (param.has("axis")) {
val axis = param.get("axis", "")
return context.getString(
R.string.qualified_button_stick_axis,
toggle,
inverted,
invert,
axis
)
}
if (param.has("button")) {
val button = param.get("button", "")
return context.getString(R.string.qualified_button, turbo, toggle, inverted, button)
}
}
return context.getString(R.string.unknown)
}
protected fun analogToText(param: ParamPackage, direction: String): String {
if (!param.has("engine")) {
return context.getString(R.string.not_set)
}
if (param.get("engine", "") == "analog_from_button") {
return buttonToText(ParamPackage(param.get(direction, "")))
}
if (!param.has("axis_x") || !param.has("axis_y")) {
return context.getString(R.string.unknown)
}
val xAxis = param.get("axis_x", "")
val yAxis = param.get("axis_y", "")
val xInvert = param.get("invert_x", "+") == "-"
val yInvert = param.get("invert_y", "+") == "-"
if (direction == "modifier") {
return context.getString(R.string.unused)
}
when (direction) {
"up" -> {
val yInvertString = if (yInvert) "+" else "-"
return context.getString(R.string.qualified_axis, yAxis, yInvertString)
}
"down" -> {
val yInvertString = if (yInvert) "-" else "+"
return context.getString(R.string.qualified_axis, yAxis, yInvertString)
}
"left" -> {
val xInvertString = if (xInvert) "+" else "-"
return context.getString(R.string.qualified_axis, xAxis, xInvertString)
}
"right" -> {
val xInvertString = if (xInvert) "-" else "+"
return context.getString(R.string.qualified_axis, xAxis, xInvertString)
}
}
return context.getString(R.string.unknown)
}
}

View File

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
class IntSingleChoiceSetting(
private val intSetting: AbstractIntSetting,
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val choices: Array<String>,
val values: Array<Int>
) : SettingsItem(intSetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_INT_SINGLE_CHOICE
fun getValueAt(index: Int): Int =
if (values.indices.contains(index)) values[index] else -1
fun getChoiceAt(index: Int): String =
if (choices.indices.contains(index)) choices[index] else ""
fun getSelectedValue(needsGlobal: Boolean = false) = intSetting.getInt(needsGlobal)
fun setSelectedValue(value: Int) = intSetting.setInt(value)
val selectedValueIndex: Int
get() {
for (i in values.indices) {
if (values[i] == getSelectedValue()) {
return i
}
}
return -1
}
}

View File

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.InputType
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.utils.ParamPackage
class ModifierInputSetting(
override val playerIndex: Int,
val nativeAnalog: NativeAnalog,
@StringRes titleId: Int = 0,
titleString: String = ""
) : InputSetting(titleId, titleString) {
override val inputType = InputType.Button
override fun getSelectedValue(): String {
val analogParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
val modifierParam = ParamPackage(analogParam.get("modifier", ""))
return buttonToText(modifierParam)
}
override fun setSelectedValue(param: ParamPackage) {
val newParam = NativeInput.getStickParam(playerIndex, nativeAnalog)
newParam.set("modifier", param.serialize())
NativeInput.setStickParam(playerIndex, nativeAnalog, newParam)
}
}

View File

@ -4,13 +4,16 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
class RunnableSetting( class RunnableSetting(
titleId: Int, @StringRes titleId: Int = 0,
descriptionId: Int, titleString: String = "",
val isRuntimeRunnable: Boolean, @StringRes descriptionId: Int = 0,
descriptionString: String = "",
val isRunnable: Boolean,
@DrawableRes val iconId: Int = 0, @DrawableRes val iconId: Int = 0,
val runnable: () -> Unit val runnable: () -> Unit
) : SettingsItem(emptySetting, titleId, descriptionId) { ) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_RUNNABLE override val type = TYPE_RUNNABLE
} }

View File

@ -3,8 +3,12 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.NativeLibrary 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.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
@ -23,13 +27,34 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
*/ */
abstract class SettingsItem( abstract class SettingsItem(
val setting: AbstractSetting, val setting: AbstractSetting,
val nameId: Int, @StringRes val titleId: Int,
val descriptionId: Int val titleString: String,
@StringRes val descriptionId: Int,
val descriptionString: String
) { ) {
abstract val type: Int abstract val type: Int
val title: String by lazy {
if (titleId != 0) {
return@lazy YuzuApplication.appContext.getString(titleId)
}
return@lazy titleString
}
val description: String by lazy {
if (descriptionId != 0) {
return@lazy YuzuApplication.appContext.getString(descriptionId)
}
return@lazy descriptionString
}
val isEditable: Boolean val isEditable: Boolean
get() { get() {
// Can't change docked mode toggle when using handheld mode
if (setting.key == BooleanSetting.USE_DOCKED_MODE.key) {
return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld
}
// Can't edit settings that aren't saveable in per-game config even if they are switchable // Can't edit settings that aren't saveable in per-game config even if they are switchable
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) { if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
return false return false
@ -59,6 +84,9 @@ abstract class SettingsItem(
const val TYPE_STRING_SINGLE_CHOICE = 5 const val TYPE_STRING_SINGLE_CHOICE = 5
const val TYPE_DATETIME_SETTING = 6 const val TYPE_DATETIME_SETTING = 6
const val TYPE_RUNNABLE = 7 const val TYPE_RUNNABLE = 7
const val TYPE_INPUT = 8
const val TYPE_INT_SINGLE_CHOICE = 9
const val TYPE_INPUT_PROFILE = 10
const val FASTMEM_COMBINED = "fastmem_combined" const val FASTMEM_COMBINED = "fastmem_combined"
@ -80,237 +108,242 @@ abstract class SettingsItem(
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_USE_SPEED_LIMIT, BooleanSetting.RENDERER_USE_SPEED_LIMIT,
R.string.frame_limit_enable, titleId = R.string.frame_limit_enable,
R.string.frame_limit_enable_description descriptionId = R.string.frame_limit_enable_description
) )
) )
put( put(
SliderSetting( SliderSetting(
ShortSetting.RENDERER_SPEED_LIMIT, ShortSetting.RENDERER_SPEED_LIMIT,
R.string.frame_limit_slider, titleId = R.string.frame_limit_slider,
R.string.frame_limit_slider_description, descriptionId = R.string.frame_limit_slider_description,
1, min = 1,
400, max = 400,
"%" units = "%"
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.CPU_BACKEND, IntSetting.CPU_BACKEND,
R.string.cpu_backend, titleId = R.string.cpu_backend,
0, choicesId = R.array.cpuBackendArm64Names,
R.array.cpuBackendArm64Names, valuesId = R.array.cpuBackendArm64Values
R.array.cpuBackendArm64Values
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.CPU_ACCURACY, IntSetting.CPU_ACCURACY,
R.string.cpu_accuracy, titleId = R.string.cpu_accuracy,
0, choicesId = R.array.cpuAccuracyNames,
R.array.cpuAccuracyNames, valuesId = R.array.cpuAccuracyValues
R.array.cpuAccuracyValues
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.PICTURE_IN_PICTURE, BooleanSetting.PICTURE_IN_PICTURE,
R.string.picture_in_picture, titleId = R.string.picture_in_picture,
R.string.picture_in_picture_description descriptionId = R.string.picture_in_picture_description
) )
) )
val dockedModeSetting = object : AbstractBooleanSetting {
override val key = BooleanSetting.USE_DOCKED_MODE.key
override fun getBoolean(needsGlobal: Boolean): Boolean {
if (NativeInput.getStyleIndex(0) == NpadStyleIndex.Handheld) {
return false
}
return BooleanSetting.USE_DOCKED_MODE.getBoolean(needsGlobal)
}
override fun setBoolean(value: Boolean) =
BooleanSetting.USE_DOCKED_MODE.setBoolean(value)
override val defaultValue = BooleanSetting.USE_DOCKED_MODE.defaultValue
override fun getValueAsString(needsGlobal: Boolean): String =
BooleanSetting.USE_DOCKED_MODE.getValueAsString(needsGlobal)
override fun reset() = BooleanSetting.USE_DOCKED_MODE.reset()
}
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.USE_DOCKED_MODE, dockedModeSetting,
R.string.use_docked_mode, titleId = R.string.use_docked_mode,
R.string.use_docked_mode_description descriptionId = R.string.use_docked_mode_description
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.REGION_INDEX, IntSetting.REGION_INDEX,
R.string.emulated_region, titleId = R.string.emulated_region,
0, choicesId = R.array.regionNames,
R.array.regionNames, valuesId = R.array.regionValues
R.array.regionValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.LANGUAGE_INDEX, IntSetting.LANGUAGE_INDEX,
R.string.emulated_language, titleId = R.string.emulated_language,
0, choicesId = R.array.languageNames,
R.array.languageNames, valuesId = R.array.languageValues
R.array.languageValues
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.USE_CUSTOM_RTC, BooleanSetting.USE_CUSTOM_RTC,
R.string.use_custom_rtc, titleId = R.string.use_custom_rtc,
R.string.use_custom_rtc_description descriptionId = R.string.use_custom_rtc_description
) )
) )
put(DateTimeSetting(LongSetting.CUSTOM_RTC, R.string.set_custom_rtc, 0)) put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc))
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_ACCURACY, IntSetting.RENDERER_ACCURACY,
R.string.renderer_accuracy, titleId = R.string.renderer_accuracy,
0, choicesId = R.array.rendererAccuracyNames,
R.array.rendererAccuracyNames, valuesId = R.array.rendererAccuracyValues
R.array.rendererAccuracyValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_RESOLUTION, IntSetting.RENDERER_RESOLUTION,
R.string.renderer_resolution, titleId = R.string.renderer_resolution,
0, choicesId = R.array.rendererResolutionNames,
R.array.rendererResolutionNames, valuesId = R.array.rendererResolutionValues
R.array.rendererResolutionValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_VSYNC, IntSetting.RENDERER_VSYNC,
R.string.renderer_vsync, titleId = R.string.renderer_vsync,
0, choicesId = R.array.rendererVSyncNames,
R.array.rendererVSyncNames, valuesId = R.array.rendererVSyncValues
R.array.rendererVSyncValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_SCALING_FILTER, IntSetting.RENDERER_SCALING_FILTER,
R.string.renderer_scaling_filter, titleId = R.string.renderer_scaling_filter,
0, choicesId = R.array.rendererScalingFilterNames,
R.array.rendererScalingFilterNames, valuesId = R.array.rendererScalingFilterValues
R.array.rendererScalingFilterValues
) )
) )
put( put(
SliderSetting( SliderSetting(
IntSetting.FSR_SHARPENING_SLIDER, IntSetting.FSR_SHARPENING_SLIDER,
R.string.fsr_sharpness, titleId = R.string.fsr_sharpness,
R.string.fsr_sharpness_description, descriptionId = R.string.fsr_sharpness_description,
0, units = "%"
100,
"%"
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_ANTI_ALIASING, IntSetting.RENDERER_ANTI_ALIASING,
R.string.renderer_anti_aliasing, titleId = R.string.renderer_anti_aliasing,
0, choicesId = R.array.rendererAntiAliasingNames,
R.array.rendererAntiAliasingNames, valuesId = R.array.rendererAntiAliasingValues
R.array.rendererAntiAliasingValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT, IntSetting.RENDERER_SCREEN_LAYOUT,
R.string.renderer_screen_layout, titleId = R.string.renderer_screen_layout,
0, choicesId = R.array.rendererScreenLayoutNames,
R.array.rendererScreenLayoutNames, valuesId = R.array.rendererScreenLayoutValues
R.array.rendererScreenLayoutValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO, IntSetting.RENDERER_ASPECT_RATIO,
R.string.renderer_aspect_ratio, titleId = R.string.renderer_aspect_ratio,
0, choicesId = R.array.rendererAspectRatioNames,
R.array.rendererAspectRatioNames, valuesId = R.array.rendererAspectRatioValues
R.array.rendererAspectRatioValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.VERTICAL_ALIGNMENT, IntSetting.VERTICAL_ALIGNMENT,
R.string.vertical_alignment, titleId = R.string.vertical_alignment,
0, descriptionId = 0,
R.array.verticalAlignmentEntries, choicesId = R.array.verticalAlignmentEntries,
R.array.verticalAlignmentValues valuesId = R.array.verticalAlignmentValues
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE, BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE,
R.string.use_disk_shader_cache, titleId = R.string.use_disk_shader_cache,
R.string.use_disk_shader_cache_description descriptionId = R.string.use_disk_shader_cache_description
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_FORCE_MAX_CLOCK, BooleanSetting.RENDERER_FORCE_MAX_CLOCK,
R.string.renderer_force_max_clock, titleId = R.string.renderer_force_max_clock,
R.string.renderer_force_max_clock_description descriptionId = R.string.renderer_force_max_clock_description
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS, BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
R.string.renderer_asynchronous_shaders, titleId = R.string.renderer_asynchronous_shaders,
R.string.renderer_asynchronous_shaders_description descriptionId = R.string.renderer_asynchronous_shaders_description
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_REACTIVE_FLUSHING, BooleanSetting.RENDERER_REACTIVE_FLUSHING,
R.string.renderer_reactive_flushing, titleId = R.string.renderer_reactive_flushing,
R.string.renderer_reactive_flushing_description descriptionId = R.string.renderer_reactive_flushing_description
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.MAX_ANISOTROPY, IntSetting.MAX_ANISOTROPY,
R.string.anisotropic_filtering, titleId = R.string.anisotropic_filtering,
R.string.anisotropic_filtering_description, descriptionId = R.string.anisotropic_filtering_description,
R.array.anisoEntries, choicesId = R.array.anisoEntries,
R.array.anisoValues valuesId = R.array.anisoValues
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.AUDIO_OUTPUT_ENGINE, IntSetting.AUDIO_OUTPUT_ENGINE,
R.string.audio_output_engine, titleId = R.string.audio_output_engine,
0, choicesId = R.array.outputEngineEntries,
R.array.outputEngineEntries, valuesId = R.array.outputEngineValues
R.array.outputEngineValues
) )
) )
put( put(
SliderSetting( SliderSetting(
ByteSetting.AUDIO_VOLUME, ByteSetting.AUDIO_VOLUME,
R.string.audio_volume, titleId = R.string.audio_volume,
R.string.audio_volume_description, descriptionId = R.string.audio_volume_description,
0, units = "%"
100,
"%"
) )
) )
put( put(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_BACKEND, IntSetting.RENDERER_BACKEND,
R.string.renderer_api, titleId = R.string.renderer_api,
0, choicesId = R.array.rendererApiNames,
R.array.rendererApiNames, valuesId = R.array.rendererApiValues
R.array.rendererApiValues
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.RENDERER_DEBUG, BooleanSetting.RENDERER_DEBUG,
R.string.renderer_debug, titleId = R.string.renderer_debug,
R.string.renderer_debug_description descriptionId = R.string.renderer_debug_description
) )
) )
put( put(
SwitchSetting( SwitchSetting(
BooleanSetting.CPU_DEBUG_MODE, BooleanSetting.CPU_DEBUG_MODE,
R.string.cpu_debug_mode, titleId = R.string.cpu_debug_mode,
R.string.cpu_debug_mode_description descriptionId = R.string.cpu_debug_mode_description
) )
) )
@ -346,7 +379,7 @@ abstract class SettingsItem(
override fun reset() = setBoolean(defaultValue) override fun reset() = setBoolean(defaultValue)
} }
put(SwitchSetting(fastmem, R.string.fastmem, 0)) put(SwitchSetting(fastmem, R.string.fastmem))
} }
} }
} }

View File

@ -3,16 +3,20 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.ArrayRes
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
class SingleChoiceSetting( class SingleChoiceSetting(
setting: AbstractSetting, setting: AbstractSetting,
titleId: Int, @StringRes titleId: Int = 0,
descriptionId: Int, titleString: String = "",
val choicesId: Int, @StringRes descriptionId: Int = 0,
val valuesId: Int descriptionString: String = "",
) : SettingsItem(setting, titleId, descriptionId) { @ArrayRes val choicesId: Int,
@ArrayRes val valuesId: Int
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SINGLE_CHOICE override val type = TYPE_SINGLE_CHOICE
fun getSelectedValue(needsGlobal: Boolean = false) = fun getSelectedValue(needsGlobal: Boolean = false) =

View File

@ -3,6 +3,7 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractByteSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractFloatSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
@ -12,12 +13,14 @@ import kotlin.math.roundToInt
class SliderSetting( class SliderSetting(
setting: AbstractSetting, setting: AbstractSetting,
titleId: Int, @StringRes titleId: Int = 0,
descriptionId: Int, titleString: String = "",
val min: Int, @StringRes descriptionId: Int = 0,
val max: Int, descriptionString: String = "",
val units: String val min: Int = 0,
) : SettingsItem(setting, titleId, descriptionId) { val max: Int = 100,
val units: String = ""
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SLIDER override val type = TYPE_SLIDER
fun getSelectedValue(needsGlobal: Boolean = false) = fun getSelectedValue(needsGlobal: Boolean = false) =

View File

@ -3,15 +3,18 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
class StringSingleChoiceSetting( class StringSingleChoiceSetting(
private val stringSetting: AbstractStringSetting, private val stringSetting: AbstractStringSetting,
titleId: Int, @StringRes titleId: Int = 0,
descriptionId: Int, titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val choices: Array<String>, val choices: Array<String>,
val values: Array<String> val values: Array<String>
) : SettingsItem(stringSetting, titleId, descriptionId) { ) : SettingsItem(stringSetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_STRING_SINGLE_CHOICE override val type = TYPE_STRING_SINGLE_CHOICE
fun getValueAt(index: Int): String = fun getValueAt(index: Int): String =
@ -20,7 +23,7 @@ class StringSingleChoiceSetting(
fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal) fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
fun setSelectedValue(value: String) = stringSetting.setString(value) fun setSelectedValue(value: String) = stringSetting.setString(value)
val selectValueIndex: Int val selectedValueIndex: Int
get() { get() {
for (i in values.indices) { for (i in values.indices) {
if (values[i] == getSelectedValue()) { if (values[i] == getSelectedValue()) {

View File

@ -8,10 +8,12 @@ import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
class SubmenuSetting( class SubmenuSetting(
@StringRes titleId: Int, @StringRes titleId: Int = 0,
@StringRes descriptionId: Int, titleString: String = "",
@DrawableRes val iconId: Int, @StringRes descriptionId: Int = 0,
descriptionString: String = "",
@DrawableRes val iconId: Int = 0,
val menuKey: Settings.MenuTag val menuKey: Settings.MenuTag
) : SettingsItem(emptySetting, titleId, descriptionId) { ) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SUBMENU override val type = TYPE_SUBMENU
} }

View File

@ -3,15 +3,18 @@
package org.yuzu.yuzu_emu.features.settings.model.view package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
class SwitchSetting( class SwitchSetting(
setting: AbstractSetting, setting: AbstractSetting,
titleId: Int, @StringRes titleId: Int = 0,
descriptionId: Int titleString: String = "",
) : SettingsItem(setting, titleId, descriptionId) { @StringRes descriptionId: Int = 0,
descriptionString: String = ""
) : SettingsItem(setting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_SWITCH override val type = TYPE_SWITCH
fun getIsChecked(needsGlobal: Boolean = false): Boolean { fun getIsChecked(needsGlobal: Boolean = false): Boolean {

View File

@ -0,0 +1,300 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.app.Dialog
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogMappingBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.features.input.model.NativeButton
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.ParamPackage
class InputDialogFragment : DialogFragment() {
private var inputAccepted = false
private var position: Int = 0
private lateinit var inputSetting: InputSetting
private lateinit var binding: DialogMappingBinding
private val settingsViewModel: SettingsViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (settingsViewModel.clickedItem == null) dismiss()
position = requireArguments().getInt(POSITION)
InputHandler.updateControllerData()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
inputSetting = settingsViewModel.clickedItem as InputSetting
binding = DialogMappingBinding.inflate(layoutInflater)
val builder = MaterialAlertDialogBuilder(requireContext())
.setPositiveButton(android.R.string.cancel) { _, _ ->
NativeInput.stopMapping()
dismiss()
}
.setView(binding.root)
val playButtonMapAnimation = { twoDirections: Boolean ->
val stickAnimation: AnimatedVectorDrawable
val buttonAnimation: AnimatedVectorDrawable
binding.imageStickAnimation.apply {
val anim = if (twoDirections) {
R.drawable.stick_two_direction_anim
} else {
R.drawable.stick_one_direction_anim
}
setBackgroundResource(anim)
stickAnimation = background as AnimatedVectorDrawable
}
binding.imageButtonAnimation.apply {
setBackgroundResource(R.drawable.button_anim)
buttonAnimation = background as AnimatedVectorDrawable
}
stickAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
buttonAnimation.start()
}
})
buttonAnimation.registerAnimationCallback(object : Animatable2.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
stickAnimation.start()
}
})
stickAnimation.start()
}
when (val setting = inputSetting) {
is AnalogInputSetting -> {
when (setting.nativeAnalog) {
NativeAnalog.LStick -> builder.setTitle(
getString(R.string.map_control, getString(R.string.left_stick))
)
NativeAnalog.RStick -> builder.setTitle(
getString(R.string.map_control, getString(R.string.right_stick))
)
}
builder.setMessage(R.string.stick_map_description)
playButtonMapAnimation.invoke(true)
}
is ModifierInputSetting -> {
builder.setTitle(getString(R.string.map_control, setting.title))
.setMessage(R.string.button_map_description)
playButtonMapAnimation.invoke(false)
}
is ButtonInputSetting -> {
if (setting.nativeButton == NativeButton.DUp ||
setting.nativeButton == NativeButton.DDown ||
setting.nativeButton == NativeButton.DLeft ||
setting.nativeButton == NativeButton.DRight
) {
builder.setTitle(getString(R.string.map_dpad_direction, setting.title))
} else {
builder.setTitle(getString(R.string.map_control, setting.title))
}
builder.setMessage(R.string.button_map_description)
playButtonMapAnimation.invoke(false)
}
}
return builder.create()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.requestFocus()
view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
dialog?.setOnKeyListener { _, _, keyEvent -> onKeyEvent(keyEvent) }
binding.root.setOnGenericMotionListener { _, motionEvent -> onMotionEvent(motionEvent) }
NativeInput.beginMapping(inputSetting.inputType.int)
}
private fun onKeyEvent(event: KeyEvent): Boolean {
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
) {
return false
}
val action = when (event.action) {
KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
else -> return false
}
val controllerData =
InputHandler.androidControllers[event.device.controllerNumber] ?: return false
NativeInput.onGamePadButtonEvent(
controllerData.getGUID(),
controllerData.getPort(),
event.keyCode,
action
)
onInputReceived(event.device)
return true
}
private fun onMotionEvent(event: MotionEvent): Boolean {
if (event.source and InputDevice.SOURCE_JOYSTICK != InputDevice.SOURCE_JOYSTICK &&
event.source and InputDevice.SOURCE_GAMEPAD != InputDevice.SOURCE_GAMEPAD
) {
return false
}
// Temp workaround for DPads that give both axis and button input. The input system can't
// take in a specific axis direction for a binding so you lose half of the directions for a DPad.
val controllerData =
InputHandler.androidControllers[event.device.controllerNumber] ?: return false
event.device.motionRanges.forEach {
NativeInput.onGamePadAxisEvent(
controllerData.getGUID(),
controllerData.getPort(),
it.axis,
event.getAxisValue(it.axis)
)
onInputReceived(event.device)
}
return true
}
private fun onInputReceived(device: InputDevice) {
val params = ParamPackage(NativeInput.getNextInput())
if (params.has("engine") && isInputAcceptable(params) && !inputAccepted) {
inputAccepted = true
setResult(params, device)
}
}
private fun setResult(params: ParamPackage, device: InputDevice) {
NativeInput.stopMapping()
params.set("display", "${device.name} ${params.get("port", 0)}")
when (val item = settingsViewModel.clickedItem as InputSetting) {
is ModifierInputSetting,
is ButtonInputSetting -> {
// Invert DPad up and left bindings by default
val tempSetting = inputSetting as? ButtonInputSetting
if (tempSetting != null) {
if (tempSetting.nativeButton == NativeButton.DUp ||
tempSetting.nativeButton == NativeButton.DLeft &&
params.has("axis")
) {
params.set("invert", "-")
}
}
item.setSelectedValue(params)
settingsViewModel.setAdapterItemChanged(position)
}
is AnalogInputSetting -> {
var analogParam = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
analogParam = adjustAnalogParam(params, analogParam, item.analogDirection.param)
// Invert Y-Axis by default
analogParam.set("invert_y", "-")
item.setSelectedValue(analogParam)
settingsViewModel.setReloadListAndNotifyDataset(true)
}
}
dismiss()
}
private fun adjustAnalogParam(
inputParam: ParamPackage,
analogParam: ParamPackage,
buttonName: String
): ParamPackage {
// The poller returned a complete axis, so set all the buttons
if (inputParam.has("axis_x") && inputParam.has("axis_y")) {
return inputParam
}
// Check if the current configuration has either no engine or an axis binding.
// Clears out the old binding and adds one with analog_from_button.
if (!analogParam.has("engine") || analogParam.has("axis_x") || analogParam.has("axis_y")) {
analogParam.clear()
analogParam.set("engine", "analog_from_button")
}
analogParam.set(buttonName, inputParam.serialize())
return analogParam
}
private fun isInputAcceptable(params: ParamPackage): Boolean {
if (InputHandler.registeredControllers.size == 1) {
return true
}
if (params.has("motion")) {
return true
}
val currentDevice = settingsViewModel.getCurrentDeviceParams(params)
if (currentDevice.get("engine", "any") == "any") {
return true
}
val guidMatch = params.get("guid", "") == currentDevice.get("guid", "") ||
params.get("guid", "") == currentDevice.get("guid2", "")
return params.get("engine", "") == currentDevice.get("engine", "") &&
guidMatch &&
params.get("port", 0) == currentDevice.get("port", 0)
}
companion object {
const val TAG = "InputDialogFragment"
const val POSITION = "Position"
fun newInstance(
inputMappingViewModel: SettingsViewModel,
setting: InputSetting,
position: Int
): InputDialogFragment {
inputMappingViewModel.clickedItem = setting
val args = Bundle()
args.putInt(POSITION, position)
val fragment = InputDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.AbstractListAdapter
import org.yuzu.yuzu_emu.databinding.ListItemInputProfileBinding
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
import org.yuzu.yuzu_emu.R
class InputProfileAdapter(options: List<ProfileItem>) :
AbstractListAdapter<ProfileItem, AbstractViewHolder<ProfileItem>>(options) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AbstractViewHolder<ProfileItem> {
ListItemInputProfileBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return InputProfileViewHolder(it) }
}
inner class InputProfileViewHolder(val binding: ListItemInputProfileBinding) :
AbstractViewHolder<ProfileItem>(binding) {
override fun bind(model: ProfileItem) {
when (model) {
is ExistingProfileItem -> {
binding.title.text = model.name
binding.buttonNew.visibility = View.GONE
binding.buttonDelete.visibility = View.VISIBLE
binding.buttonDelete.setOnClickListener { model.deleteProfile.invoke() }
binding.buttonSave.visibility = View.VISIBLE
binding.buttonSave.setOnClickListener { model.saveProfile.invoke() }
binding.buttonLoad.visibility = View.VISIBLE
binding.buttonLoad.setOnClickListener { model.loadProfile.invoke() }
}
is NewProfileItem -> {
binding.title.text = model.name
binding.buttonNew.visibility = View.VISIBLE
binding.buttonNew.setOnClickListener { model.createNewProfile.invoke() }
binding.buttonSave.visibility = View.GONE
binding.buttonDelete.visibility = View.GONE
binding.buttonLoad.visibility = View.GONE
}
}
}
}
}
sealed interface ProfileItem {
val name: String
}
data class NewProfileItem(
val createNewProfile: () -> Unit
) : ProfileItem {
override val name: String = YuzuApplication.appContext.getString(R.string.create_new_profile)
}
data class ExistingProfileItem(
override val name: String,
val deleteProfile: () -> Unit,
val saveProfile: () -> Unit,
val loadProfile: () -> Unit
) : ProfileItem

View File

@ -0,0 +1,155 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogInputProfilesBinding
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
class InputProfileDialogFragment : DialogFragment() {
private var position = 0
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var binding: DialogInputProfilesBinding
private lateinit var setting: InputProfileSetting
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
position = requireArguments().getInt(POSITION)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogInputProfilesBinding.inflate(layoutInflater)
setting = settingsViewModel.clickedItem as InputProfileSetting
val options = mutableListOf<ProfileItem>().apply {
add(
NewProfileItem(
createNewProfile = {
NewInputProfileDialogFragment.newInstance(
settingsViewModel,
setting,
position
).show(parentFragmentManager, NewInputProfileDialogFragment.TAG)
dismiss()
}
)
)
val onActionDismiss = {
settingsViewModel.setReloadListAndNotifyDataset(true)
dismiss()
}
setting.getProfileNames().forEach {
add(
ExistingProfileItem(
it,
deleteProfile = {
settingsViewModel.setShouldShowDeleteProfileDialog(it)
},
saveProfile = {
if (!setting.saveProfile(it)) {
Toast.makeText(
requireContext(),
R.string.failed_to_save_profile,
Toast.LENGTH_SHORT
).show()
}
onActionDismiss.invoke()
},
loadProfile = {
if (!setting.loadProfile(it)) {
Toast.makeText(
requireContext(),
R.string.failed_to_load_profile,
Toast.LENGTH_SHORT
).show()
}
onActionDismiss.invoke()
}
)
)
}
}
binding.listProfiles.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = InputProfileAdapter(options)
}
return MaterialAlertDialogBuilder(requireContext())
.setView(binding.root)
.create()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldShowDeleteProfileDialog.collect {
if (it.isNotEmpty()) {
MessageDialogFragment.newInstance(
activity = requireActivity(),
titleId = R.string.delete_input_profile,
descriptionId = R.string.delete_input_profile_description,
positiveAction = {
setting.deleteProfile(it)
settingsViewModel.setReloadListAndNotifyDataset(true)
},
negativeAction = {},
negativeButtonTitleId = android.R.string.cancel
).show(parentFragmentManager, MessageDialogFragment.TAG)
settingsViewModel.setShouldShowDeleteProfileDialog("")
dismiss()
}
}
}
}
}
companion object {
const val TAG = "InputProfileDialogFragment"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
profileSetting: InputProfileSetting,
position: Int
): InputProfileDialogFragment {
settingsViewModel.clickedItem = profileSetting
val args = Bundle()
args.putInt(POSITION, position)
val fragment = InputProfileDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.app.Dialog
import android.os.Bundle
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
import org.yuzu.yuzu_emu.R
class NewInputProfileDialogFragment : DialogFragment() {
private var position = 0
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var binding: DialogEditTextBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
position = requireArguments().getInt(POSITION)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogEditTextBinding.inflate(layoutInflater)
val setting = settingsViewModel.clickedItem as InputProfileSetting
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.enter_profile_name)
.setPositiveButton(android.R.string.ok) { _, _ ->
val profileName = binding.editText.text.toString()
if (!setting.isProfileNameValid(profileName)) {
Toast.makeText(
requireContext(),
R.string.invalid_profile_name,
Toast.LENGTH_SHORT
).show()
return@setPositiveButton
}
if (!setting.createProfile(profileName)) {
Toast.makeText(
requireContext(),
R.string.profile_name_already_exists,
Toast.LENGTH_SHORT
).show()
} else {
settingsViewModel.setAdapterItemChanged(position)
}
}
.setNegativeButton(android.R.string.cancel, null)
.setView(binding.root)
.show()
}
companion object {
const val TAG = "NewInputProfileDialogFragment"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
profileSetting: InputProfileSetting,
position: Int
): NewInputProfileDialogFragment {
settingsViewModel.clickedItem = profileSetting
val args = Bundle()
args.putInt(POSITION, position)
val fragment = NewInputProfileDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -25,9 +25,9 @@ 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
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
import org.yuzu.yuzu_emu.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.*
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
@ -137,6 +137,7 @@ class SettingsActivity : AppCompatActivity() {
super.onStop() super.onStop()
Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...") Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
if (isFinishing) { if (isFinishing) {
NativeInput.reloadInputDevices()
NativeLibrary.applySettings() NativeLibrary.applySettings()
if (args.game == null) { if (args.game == null) {
NativeConfig.saveGlobalConfig() NativeConfig.saveGlobalConfig()

View File

@ -8,12 +8,11 @@ import android.icu.util.Calendar
import android.icu.util.TimeZone import android.icu.util.TimeZone
import android.text.format.DateFormat import android.text.format.DateFormat
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -21,16 +20,18 @@ import androidx.recyclerview.widget.ListAdapter
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.SettingsNavigationDirections import org.yuzu.yuzu_emu.SettingsNavigationDirections
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.features.settings.ui.viewholder.* import org.yuzu.yuzu_emu.features.settings.ui.viewholder.*
import org.yuzu.yuzu_emu.fragments.SettingsDialogFragment import org.yuzu.yuzu_emu.utils.ParamPackage
import org.yuzu.yuzu_emu.model.SettingsViewModel
class SettingsAdapter( class SettingsAdapter(
private val fragment: Fragment, private val fragment: Fragment,
@ -41,19 +42,6 @@ class SettingsAdapter(
private val settingsViewModel: SettingsViewModel private val settingsViewModel: SettingsViewModel
get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java] get() = ViewModelProvider(fragment.requireActivity())[SettingsViewModel::class.java]
init {
fragment.viewLifecycleOwner.lifecycleScope.launch {
fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
settingsViewModel.adapterItemChanged.collect {
if (it != -1) {
notifyItemChanged(it)
settingsViewModel.setAdapterItemChanged(-1)
}
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
@ -85,8 +73,19 @@ class SettingsAdapter(
RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this) RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)
} }
SettingsItem.TYPE_INPUT -> {
InputViewHolder(ListItemSettingInputBinding.inflate(inflater), this)
}
SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_INPUT_PROFILE -> {
InputProfileViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
else -> { else -> {
// TODO: Create an error view since we can't return null now
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this) HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
} }
} }
@ -126,6 +125,15 @@ class SettingsAdapter(
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG) ).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
} }
fun onIntSingleChoiceClick(item: IntSingleChoiceSetting, position: Int) {
SettingsDialogFragment.newInstance(
settingsViewModel,
item,
SettingsItem.TYPE_INT_SINGLE_CHOICE,
position
).show(fragment.childFragmentManager, SettingsDialogFragment.TAG)
}
fun onDateTimeClick(item: DateTimeSetting, position: Int) { fun onDateTimeClick(item: DateTimeSetting, position: Int) {
val storedTime = item.getValue() * 1000 val storedTime = item.getValue() * 1000
@ -185,6 +193,205 @@ class SettingsAdapter(
fragment.view?.findNavController()?.navigate(action) fragment.view?.findNavController()?.navigate(action)
} }
fun onInputProfileClick(item: InputProfileSetting, position: Int) {
InputProfileDialogFragment.newInstance(
settingsViewModel,
item,
position
).show(fragment.childFragmentManager, InputProfileDialogFragment.TAG)
}
fun onInputClick(item: InputSetting, position: Int) {
InputDialogFragment.newInstance(
settingsViewModel,
item,
position
).show(fragment.childFragmentManager, InputDialogFragment.TAG)
}
fun onInputOptionsClick(anchor: View, item: InputSetting, position: Int) {
val popup = PopupMenu(context, anchor)
popup.menuInflater.inflate(R.menu.menu_input_options, popup.menu)
popup.menu.apply {
val invertAxis = findItem(R.id.invert_axis)
val invertButton = findItem(R.id.invert_button)
val toggleButton = findItem(R.id.toggle_button)
val turboButton = findItem(R.id.turbo_button)
val setThreshold = findItem(R.id.set_threshold)
val toggleAxis = findItem(R.id.toggle_axis)
when (item) {
is AnalogInputSetting -> {
val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
invertAxis.isVisible = true
invertAxis.isCheckable = true
invertAxis.isChecked = when (item.analogDirection) {
AnalogDirection.Left, AnalogDirection.Right -> {
params.get("invert_x", "+") == "-"
}
AnalogDirection.Up, AnalogDirection.Down -> {
params.get("invert_y", "+") == "-"
}
}
invertAxis.setOnMenuItemClickListener {
if (item.analogDirection == AnalogDirection.Left ||
item.analogDirection == AnalogDirection.Right
) {
val invertValue = params.get("invert_x", "+") == "-"
val invertString = if (invertValue) "+" else "-"
params.set("invert_x", invertString)
} else if (
item.analogDirection == AnalogDirection.Up ||
item.analogDirection == AnalogDirection.Down
) {
val invertValue = params.get("invert_y", "+") == "-"
val invertString = if (invertValue) "+" else "-"
params.set("invert_y", invertString)
}
true
}
popup.setOnDismissListener {
NativeInput.setStickParam(item.playerIndex, item.nativeAnalog, params)
settingsViewModel.setDatasetChanged(true)
}
}
is ButtonInputSetting -> {
val params = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
if (params.has("code") || params.has("button") || params.has("hat")) {
val buttonInvert = params.get("inverted", false)
invertButton.isVisible = true
invertButton.isCheckable = true
invertButton.isChecked = buttonInvert
invertButton.setOnMenuItemClickListener {
params.set("inverted", !buttonInvert)
true
}
val toggle = params.get("toggle", false)
toggleButton.isVisible = true
toggleButton.isCheckable = true
toggleButton.isChecked = toggle
toggleButton.setOnMenuItemClickListener {
params.set("toggle", !toggle)
true
}
val turbo = params.get("turbo", false)
turboButton.isVisible = true
turboButton.isCheckable = true
turboButton.isChecked = turbo
turboButton.setOnMenuItemClickListener {
params.set("turbo", !turbo)
true
}
} else if (params.has("axis")) {
val axisInvert = params.get("invert", "+") == "-"
invertAxis.isVisible = true
invertAxis.isCheckable = true
invertAxis.isChecked = axisInvert
invertAxis.setOnMenuItemClickListener {
params.set("invert", if (!axisInvert) "-" else "+")
true
}
val buttonInvert = params.get("inverted", false)
invertButton.isVisible = true
invertButton.isCheckable = true
invertButton.isChecked = buttonInvert
invertButton.setOnMenuItemClickListener {
params.set("inverted", !buttonInvert)
true
}
setThreshold.isVisible = true
val thresholdSetting = object : AbstractIntSetting {
override val key = ""
override fun getInt(needsGlobal: Boolean): Int =
(params.get("threshold", 0.5f) * 100).toInt()
override fun setInt(value: Int) {
params.set("threshold", value.toFloat() / 100)
NativeInput.setButtonParam(
item.playerIndex,
item.nativeButton,
params
)
}
override val defaultValue = 50
override fun getValueAsString(needsGlobal: Boolean): String =
getInt(needsGlobal).toString()
override fun reset() = setInt(defaultValue)
}
setThreshold.setOnMenuItemClickListener {
onSliderClick(
SliderSetting(thresholdSetting, R.string.set_threshold),
position
)
true
}
val axisToggle = params.get("toggle", false)
toggleAxis.isVisible = true
toggleAxis.isCheckable = true
toggleAxis.isChecked = axisToggle
toggleAxis.setOnMenuItemClickListener {
params.set("toggle", !axisToggle)
true
}
}
popup.setOnDismissListener {
NativeInput.setButtonParam(item.playerIndex, item.nativeButton, params)
settingsViewModel.setAdapterItemChanged(position)
}
}
is ModifierInputSetting -> {
val stickParams = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
val modifierParams = ParamPackage(stickParams.get("modifier", ""))
val invert = modifierParams.get("inverted", false)
invertButton.isVisible = true
invertButton.isCheckable = true
invertButton.isChecked = invert
invertButton.setOnMenuItemClickListener {
modifierParams.set("inverted", !invert)
stickParams.set("modifier", modifierParams.serialize())
true
}
val toggle = modifierParams.get("toggle", false)
toggleButton.isVisible = true
toggleButton.isCheckable = true
toggleButton.isChecked = toggle
toggleButton.setOnMenuItemClickListener {
modifierParams.set("toggle", !toggle)
stickParams.set("modifier", modifierParams.serialize())
true
}
popup.setOnDismissListener {
NativeInput.setStickParam(
item.playerIndex,
item.nativeAnalog,
stickParams
)
settingsViewModel.setAdapterItemChanged(position)
}
}
}
}
popup.show()
}
fun onLongClick(item: SettingsItem, position: Int): Boolean { fun onLongClick(item: SettingsItem, position: Int): Boolean {
SettingsDialogFragment.newInstance( SettingsDialogFragment.newInstance(
settingsViewModel, settingsViewModel,

View File

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.features.settings.ui
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
@ -19,11 +19,16 @@ import com.google.android.material.slider.Slider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogSliderBinding import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting
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.SingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
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.model.view.StringSingleChoiceSetting import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
import org.yuzu.yuzu_emu.model.SettingsViewModel import org.yuzu.yuzu_emu.utils.ParamPackage
class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener { class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener {
private var type = 0 private var type = 0
@ -50,9 +55,50 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.reset_setting_confirmation) .setMessage(R.string.reset_setting_confirmation)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
when (val item = settingsViewModel.clickedItem) {
is AnalogInputSetting -> {
val stickParam = NativeInput.getStickParam(
item.playerIndex,
item.nativeAnalog
)
if (stickParam.get("engine", "") == "analog_from_button") {
when (item.analogDirection) {
AnalogDirection.Up -> stickParam.erase("up")
AnalogDirection.Down -> stickParam.erase("down")
AnalogDirection.Left -> stickParam.erase("left")
AnalogDirection.Right -> stickParam.erase("right")
}
NativeInput.setStickParam(
item.playerIndex,
item.nativeAnalog,
stickParam
)
settingsViewModel.setAdapterItemChanged(position)
} else {
NativeInput.setStickParam(
item.playerIndex,
item.nativeAnalog,
ParamPackage()
)
settingsViewModel.setDatasetChanged(true)
}
}
is ButtonInputSetting -> {
NativeInput.setButtonParam(
item.playerIndex,
item.nativeButton,
ParamPackage()
)
settingsViewModel.setAdapterItemChanged(position)
}
else -> {
settingsViewModel.clickedItem!!.setting.reset() settingsViewModel.clickedItem!!.setting.reset()
settingsViewModel.setAdapterItemChanged(position) settingsViewModel.setAdapterItemChanged(position)
} }
}
}
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.create() .create()
} }
@ -61,7 +107,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
val item = settingsViewModel.clickedItem as SingleChoiceSetting val item = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getSelectionForSingleChoiceValue(item) val value = getSelectionForSingleChoiceValue(item)
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(item.nameId) .setTitle(item.title)
.setSingleChoiceItems(item.choicesId, value, this) .setSingleChoiceItems(item.choicesId, value, this)
.create() .create()
} }
@ -81,7 +127,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
} }
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(item.nameId) .setTitle(item.title)
.setView(sliderBinding.root) .setView(sliderBinding.root)
.setPositiveButton(android.R.string.ok, this) .setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, defaultCancelListener) .setNegativeButton(android.R.string.cancel, defaultCancelListener)
@ -91,8 +137,16 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as StringSingleChoiceSetting val item = settingsViewModel.clickedItem as StringSingleChoiceSetting
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(item.nameId) .setTitle(item.title)
.setSingleChoiceItems(item.choices, item.selectValueIndex, this) .setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
.create()
}
SettingsItem.TYPE_INT_SINGLE_CHOICE -> {
val item = settingsViewModel.clickedItem as IntSingleChoiceSetting
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setSingleChoiceItems(item.choices, item.selectedValueIndex, this)
.create() .create()
} }
@ -145,6 +199,12 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
scSetting.setSelectedValue(value) scSetting.setSelectedValue(value)
} }
is IntSingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as IntSingleChoiceSetting
val value = scSetting.getValueAt(which)
scSetting.setSelectedValue(value)
}
is SliderSetting -> { is SliderSetting -> {
val sliderSetting = settingsViewModel.clickedItem as SliderSetting val sliderSetting = settingsViewModel.clickedItem as SliderSetting
sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value) sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)

View File

@ -24,8 +24,9 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.model.SettingsViewModel import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
class SettingsFragment : Fragment() { class SettingsFragment : Fragment() {
@ -45,6 +46,12 @@ class SettingsFragment : Fragment() {
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
val playerIndex = getPlayerIndex()
if (playerIndex != -1) {
NativeInput.loadInputProfiles()
NativeInput.reloadInputDevices()
}
} }
override fun onCreateView( override fun onCreateView(
@ -57,8 +64,9 @@ class SettingsFragment : Fragment() {
} }
// This is using the correct scope, lint is just acting up // This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector") @SuppressLint("UnsafeRepeatOnLifecycleDetector", "NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsAdapter = SettingsAdapter(this, requireContext()) settingsAdapter = SettingsAdapter(this, requireContext())
presenter = SettingsFragmentPresenter( presenter = SettingsFragmentPresenter(
settingsViewModel, settingsViewModel,
@ -71,7 +79,17 @@ class SettingsFragment : Fragment() {
) { ) {
args.game!!.title args.game!!.title
} else { } else {
getString(args.menuTag.titleId) when (args.menuTag) {
Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> Settings.getPlayerString(1)
Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> Settings.getPlayerString(2)
Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> Settings.getPlayerString(3)
Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> Settings.getPlayerString(4)
Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> Settings.getPlayerString(5)
Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> Settings.getPlayerString(6)
Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> Settings.getPlayerString(7)
Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> Settings.getPlayerString(8)
else -> getString(args.menuTag.titleId)
}
} }
binding.listSettings.apply { binding.listSettings.apply {
adapter = settingsAdapter adapter = settingsAdapter
@ -93,6 +111,55 @@ class SettingsFragment : Fragment() {
} }
} }
} }
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
settingsViewModel.adapterItemChanged.collect {
if (it != -1) {
settingsAdapter?.notifyItemChanged(it)
settingsViewModel.setAdapterItemChanged(-1)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
settingsViewModel.datasetChanged.collect {
if (it) {
settingsAdapter?.notifyDataSetChanged()
settingsViewModel.setDatasetChanged(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.reloadListAndNotifyDataset.collectLatest {
if (it) {
settingsViewModel.setReloadListAndNotifyDataset(false)
presenter.loadSettingsList(true)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
settingsViewModel.shouldShowResetInputDialog.collectLatest {
if (it) {
MessageDialogFragment.newInstance(
activity = requireActivity(),
titleId = R.string.reset_mapping,
descriptionId = R.string.reset_mapping_description,
positiveAction = {
NativeInput.resetControllerMappings(getPlayerIndex())
settingsViewModel.setReloadListAndNotifyDataset(true)
},
negativeAction = {}
).show(parentFragmentManager, MessageDialogFragment.TAG)
settingsViewModel.setShouldShowResetInputDialog(false)
}
}
}
}
} }
if (args.menuTag == Settings.MenuTag.SECTION_ROOT) { if (args.menuTag == Settings.MenuTag.SECTION_ROOT) {
@ -115,6 +182,19 @@ class SettingsFragment : Fragment() {
setInsets() setInsets()
} }
private fun getPlayerIndex(): Int =
when (args.menuTag) {
Settings.MenuTag.SECTION_INPUT_PLAYER_ONE -> 0
Settings.MenuTag.SECTION_INPUT_PLAYER_TWO -> 1
Settings.MenuTag.SECTION_INPUT_PLAYER_THREE -> 2
Settings.MenuTag.SECTION_INPUT_PLAYER_FOUR -> 3
Settings.MenuTag.SECTION_INPUT_PLAYER_FIVE -> 4
Settings.MenuTag.SECTION_INPUT_PLAYER_SIX -> 5
Settings.MenuTag.SECTION_INPUT_PLAYER_SEVEN -> 6
Settings.MenuTag.SECTION_INPUT_PLAYER_EIGHT -> 7
else -> -1
}
private fun setInsets() { private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(
binding.root binding.root

View File

@ -3,11 +3,17 @@
package org.yuzu.yuzu_emu.features.settings.ui package org.yuzu.yuzu_emu.features.settings.ui
import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import android.widget.Toast import android.widget.Toast
import org.yuzu.yuzu_emu.NativeLibrary 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.input.NativeInput
import org.yuzu.yuzu_emu.features.input.model.AnalogDirection
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.features.input.model.NativeButton
import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
@ -15,18 +21,21 @@ 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.Settings import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.Settings.MenuTag
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.features.settings.model.view.* import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.model.SettingsViewModel import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
class SettingsFragmentPresenter( class SettingsFragmentPresenter(
private val settingsViewModel: SettingsViewModel, private val settingsViewModel: SettingsViewModel,
private val adapter: SettingsAdapter, private val adapter: SettingsAdapter,
private var menuTag: Settings.MenuTag private var menuTag: MenuTag
) { ) {
private var settingsList = ArrayList<SettingsItem>() private var settingsList = ArrayList<SettingsItem>()
private val context get() = YuzuApplication.appContext
// Extension for altering settings list based on each setting's properties // 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]!!
@ -53,73 +62,90 @@ class SettingsFragmentPresenter(
add(item) add(item)
} }
// Allows you to show/hide abstract settings based on the paired setting key
fun ArrayList<SettingsItem>.addAbstract(item: SettingsItem) {
val pairedSettingKey = item.setting.pairedSettingKey
if (pairedSettingKey.isNotEmpty()) {
val pairedSettingsItem =
this.firstOrNull { it.setting.key == pairedSettingKey } ?: return
val pairedSetting = pairedSettingsItem.setting as AbstractBooleanSetting
if (!pairedSetting.getBoolean(!NativeConfig.isPerGameConfigLoaded())) return
}
add(item)
}
fun onViewCreated() { fun onViewCreated() {
loadSettingsList() loadSettingsList()
} }
fun loadSettingsList() { @SuppressLint("NotifyDataSetChanged")
fun loadSettingsList(notifyDataSetChanged: Boolean = false) {
val sl = ArrayList<SettingsItem>() val sl = ArrayList<SettingsItem>()
when (menuTag) { when (menuTag) {
Settings.MenuTag.SECTION_ROOT -> addConfigSettings(sl) MenuTag.SECTION_ROOT -> addConfigSettings(sl)
Settings.MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
Settings.MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
Settings.MenuTag.SECTION_AUDIO -> addAudioSettings(sl) MenuTag.SECTION_AUDIO -> addAudioSettings(sl)
Settings.MenuTag.SECTION_THEME -> addThemeSettings(sl) MenuTag.SECTION_INPUT -> addInputSettings(sl)
Settings.MenuTag.SECTION_DEBUG -> addDebugSettings(sl) MenuTag.SECTION_INPUT_PLAYER_ONE -> addInputPlayer(sl, 0)
else -> { MenuTag.SECTION_INPUT_PLAYER_TWO -> addInputPlayer(sl, 1)
val context = YuzuApplication.appContext MenuTag.SECTION_INPUT_PLAYER_THREE -> addInputPlayer(sl, 2)
Toast.makeText( MenuTag.SECTION_INPUT_PLAYER_FOUR -> addInputPlayer(sl, 3)
context, MenuTag.SECTION_INPUT_PLAYER_FIVE -> addInputPlayer(sl, 4)
context.getString(R.string.unimplemented_menu), MenuTag.SECTION_INPUT_PLAYER_SIX -> addInputPlayer(sl, 5)
Toast.LENGTH_SHORT MenuTag.SECTION_INPUT_PLAYER_SEVEN -> addInputPlayer(sl, 6)
).show() MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7)
return MenuTag.SECTION_THEME -> addThemeSettings(sl)
} MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
} }
settingsList = sl settingsList = sl
adapter.submitList(settingsList) adapter.submitList(settingsList) {
if (notifyDataSetChanged) {
adapter.notifyDataSetChanged()
}
}
} }
private fun addConfigSettings(sl: ArrayList<SettingsItem>) { private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
sl.apply { sl.apply {
add( add(
SubmenuSetting( SubmenuSetting(
R.string.preferences_system, titleId = R.string.preferences_system,
R.string.preferences_system_description, descriptionId = R.string.preferences_system_description,
R.drawable.ic_system_settings, iconId = R.drawable.ic_system_settings,
Settings.MenuTag.SECTION_SYSTEM menuKey = MenuTag.SECTION_SYSTEM
) )
) )
add( add(
SubmenuSetting( SubmenuSetting(
R.string.preferences_graphics, titleId = R.string.preferences_graphics,
R.string.preferences_graphics_description, descriptionId = R.string.preferences_graphics_description,
R.drawable.ic_graphics, iconId = R.drawable.ic_graphics,
Settings.MenuTag.SECTION_RENDERER menuKey = MenuTag.SECTION_RENDERER
) )
) )
add( add(
SubmenuSetting( SubmenuSetting(
R.string.preferences_audio, titleId = R.string.preferences_audio,
R.string.preferences_audio_description, descriptionId = R.string.preferences_audio_description,
R.drawable.ic_audio, iconId = R.drawable.ic_audio,
Settings.MenuTag.SECTION_AUDIO menuKey = MenuTag.SECTION_AUDIO
) )
) )
add( add(
SubmenuSetting( SubmenuSetting(
R.string.preferences_debug, titleId = R.string.preferences_debug,
R.string.preferences_debug_description, descriptionId = R.string.preferences_debug_description,
R.drawable.ic_code, iconId = R.drawable.ic_code,
Settings.MenuTag.SECTION_DEBUG menuKey = MenuTag.SECTION_DEBUG
) )
) )
add( add(
RunnableSetting( RunnableSetting(
R.string.reset_to_default, titleId = R.string.reset_to_default,
R.string.reset_to_default_description, descriptionId = R.string.reset_to_default_description,
false, isRunnable = !NativeLibrary.isRunning(),
R.drawable.ic_restore iconId = R.drawable.ic_restore
) { settingsViewModel.setShouldShowResetSettingsDialog(true) } ) { settingsViewModel.setShouldShowResetSettingsDialog(true) }
) )
} }
@ -164,6 +190,671 @@ class SettingsFragmentPresenter(
} }
} }
private fun addInputSettings(sl: ArrayList<SettingsItem>) {
settingsViewModel.currentDevice = 0
if (NativeConfig.isPerGameConfigLoaded()) {
NativeInput.loadInputProfiles()
val profiles = NativeInput.getInputProfileNames().toMutableList()
profiles.add(0, "")
val prettyProfiles = profiles.toTypedArray()
prettyProfiles[0] =
context.getString(R.string.use_global_input_configuration)
sl.apply {
for (i in 0 until 8) {
add(
IntSingleChoiceSetting(
getPerGameProfileSetting(profiles, i),
titleString = getPlayerProfileString(i + 1),
choices = prettyProfiles,
values = IntArray(profiles.size) { it }.toTypedArray()
)
)
}
}
return
}
val getConnectedIcon: (Int) -> Int = { playerIndex: Int ->
if (NativeInput.getIsConnected(playerIndex)) {
R.drawable.ic_controller
} else {
R.drawable.ic_controller_disconnected
}
}
val inputSettings = NativeConfig.getInputSettings(true)
sl.apply {
add(
SubmenuSetting(
titleString = Settings.getPlayerString(1),
descriptionString = inputSettings[0].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_ONE,
iconId = getConnectedIcon(0)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(2),
descriptionString = inputSettings[1].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_TWO,
iconId = getConnectedIcon(1)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(3),
descriptionString = inputSettings[2].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_THREE,
iconId = getConnectedIcon(2)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(4),
descriptionString = inputSettings[3].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_FOUR,
iconId = getConnectedIcon(3)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(5),
descriptionString = inputSettings[4].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_FIVE,
iconId = getConnectedIcon(4)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(6),
descriptionString = inputSettings[5].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_SIX,
iconId = getConnectedIcon(5)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(7),
descriptionString = inputSettings[6].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_SEVEN,
iconId = getConnectedIcon(6)
)
)
add(
SubmenuSetting(
titleString = Settings.getPlayerString(8),
descriptionString = inputSettings[7].profileName,
menuKey = MenuTag.SECTION_INPUT_PLAYER_EIGHT,
iconId = getConnectedIcon(7)
)
)
}
}
private fun getPlayerProfileString(player: Int): String =
context.getString(R.string.player_num_profile, player)
private fun getPerGameProfileSetting(
profiles: List<String>,
playerIndex: Int
): AbstractIntSetting {
return object : AbstractIntSetting {
private val players
get() = NativeConfig.getInputSettings(false)
override val key = ""
override fun getInt(needsGlobal: Boolean): Int {
val currentProfile = players[playerIndex].profileName
profiles.forEachIndexed { i, profile ->
if (profile == currentProfile) {
return i
}
}
return 0
}
override fun setInt(value: Int) {
NativeInput.loadPerGameConfiguration(playerIndex, value, profiles[value])
NativeInput.connectControllers(playerIndex)
NativeConfig.saveControlPlayerValues()
}
override val defaultValue = 0
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
override fun reset() = setInt(defaultValue)
override var global = true
override val isRuntimeModifiable = true
override val isSaveable = true
}
}
private fun addInputPlayer(sl: ArrayList<SettingsItem>, playerIndex: Int) {
sl.apply {
val connectedSetting = object : AbstractBooleanSetting {
override val key = "connected"
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeInput.getIsConnected(playerIndex)
override fun setBoolean(value: Boolean) =
NativeInput.connectControllers(playerIndex, value)
override val defaultValue = playerIndex == 0
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean(needsGlobal).toString()
override fun reset() = setBoolean(defaultValue)
}
add(SwitchSetting(connectedSetting, R.string.connected))
val styleTags = NativeInput.getSupportedStyleTags(playerIndex)
val npadType = object : AbstractIntSetting {
override val key = "npad_type"
override fun getInt(needsGlobal: Boolean): Int {
val styleIndex = NativeInput.getStyleIndex(playerIndex)
return styleTags.indexOfFirst { it == styleIndex }
}
override fun setInt(value: Int) {
NativeInput.setStyleIndex(playerIndex, styleTags[value])
settingsViewModel.setReloadListAndNotifyDataset(true)
}
override val defaultValue = NpadStyleIndex.Fullkey.int
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
override fun reset() = setInt(defaultValue)
override val pairedSettingKey: String = "connected"
}
addAbstract(
IntSingleChoiceSetting(
npadType,
titleId = R.string.controller_type,
choices = styleTags.map { context.getString(it.nameId) }
.toTypedArray(),
values = IntArray(styleTags.size) { it }.toTypedArray()
)
)
InputHandler.updateControllerData()
val autoMappingSetting = object : AbstractIntSetting {
override val key = "auto_mapping_device"
override fun getInt(needsGlobal: Boolean): Int = -1
override fun setInt(value: Int) {
val registeredController = InputHandler.registeredControllers[value + 1]
val displayName = registeredController.get(
"display",
context.getString(R.string.unknown)
)
NativeInput.updateMappingsWithDefault(
playerIndex,
registeredController,
displayName
)
Toast.makeText(
context,
context.getString(R.string.attempted_auto_map, displayName),
Toast.LENGTH_SHORT
).show()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
override val defaultValue = -1
override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
override fun reset() = setInt(defaultValue)
override val isRuntimeModifiable: Boolean = true
}
val unknownString = context.getString(R.string.unknown)
val prettyAutoMappingControllerList = InputHandler.registeredControllers.mapNotNull {
val port = it.get("port", -1)
return@mapNotNull if (port == 100 || port == -1) {
null
} else {
it.get("display", unknownString)
}
}.toTypedArray()
add(
IntSingleChoiceSetting(
autoMappingSetting,
titleId = R.string.auto_map,
descriptionId = R.string.auto_map_description,
choices = prettyAutoMappingControllerList,
values = IntArray(prettyAutoMappingControllerList.size) { it }.toTypedArray()
)
)
val mappingFilterSetting = object : AbstractIntSetting {
override val key = "mapping_filter"
override fun getInt(needsGlobal: Boolean): Int = settingsViewModel.currentDevice
override fun setInt(value: Int) {
settingsViewModel.currentDevice = value
}
override val defaultValue = 0
override fun getValueAsString(needsGlobal: Boolean) = getInt().toString()
override fun reset() = setInt(defaultValue)
override val isRuntimeModifiable: Boolean = true
}
val prettyControllerList = InputHandler.registeredControllers.mapNotNull {
return@mapNotNull if (it.get("port", 0) == 100) {
null
} else {
it.get("display", unknownString)
}
}.toTypedArray()
add(
IntSingleChoiceSetting(
mappingFilterSetting,
titleId = R.string.input_mapping_filter,
descriptionId = R.string.input_mapping_filter_description,
choices = prettyControllerList,
values = IntArray(prettyControllerList.size) { it }.toTypedArray()
)
)
add(InputProfileSetting(playerIndex))
add(
RunnableSetting(titleId = R.string.reset_to_default, isRunnable = true) {
settingsViewModel.setShouldShowResetInputDialog(true)
}
)
val styleIndex = NativeInput.getStyleIndex(playerIndex)
// Buttons
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
add(
ButtonInputSetting(
playerIndex,
NativeButton.Capture,
R.string.button_capture
)
)
}
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.Minus, R.string.button_minus))
add(
ButtonInputSetting(
playerIndex,
NativeButton.Capture,
R.string.button_capture
)
)
}
NpadStyleIndex.JoyconRight -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.button_plus))
add(ButtonInputSetting(playerIndex, NativeButton.Home, R.string.button_home))
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.buttons))
add(ButtonInputSetting(playerIndex, NativeButton.A, R.string.button_a))
add(ButtonInputSetting(playerIndex, NativeButton.B, R.string.button_b))
add(ButtonInputSetting(playerIndex, NativeButton.X, R.string.button_x))
add(ButtonInputSetting(playerIndex, NativeButton.Y, R.string.button_y))
add(ButtonInputSetting(playerIndex, NativeButton.Plus, R.string.start_pause))
}
else -> {
// No-op
}
}
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual,
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.dpad))
add(ButtonInputSetting(playerIndex, NativeButton.DUp, R.string.up))
add(ButtonInputSetting(playerIndex, NativeButton.DDown, R.string.down))
add(ButtonInputSetting(playerIndex, NativeButton.DLeft, R.string.left))
add(ButtonInputSetting(playerIndex, NativeButton.DRight, R.string.right))
}
else -> {
// No-op
}
}
// Left stick
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual,
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.left_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
add(ButtonInputSetting(playerIndex, NativeButton.LStick, R.string.pressed))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.control_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.LStick))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.LStick))
}
else -> {
// No-op
}
}
// Right stick
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld,
NpadStyleIndex.JoyconDual,
NpadStyleIndex.JoyconRight -> {
add(HeaderSetting(R.string.right_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
add(ButtonInputSetting(playerIndex, NativeButton.RStick, R.string.pressed))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.c_stick))
addAll(getStickDirections(playerIndex, NativeAnalog.RStick))
addAll(getExtraStickSettings(playerIndex, NativeAnalog.RStick))
}
else -> {
// No-op
}
}
// L/R, ZL/ZR, and SL/SR
when (styleIndex) {
NpadStyleIndex.Fullkey,
NpadStyleIndex.Handheld -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
}
NpadStyleIndex.JoyconDual -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLLeft,
R.string.button_sl_left
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRLeft,
R.string.button_sr_left
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLRight,
R.string.button_sl_right
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRRight,
R.string.button_sr_right
)
)
}
NpadStyleIndex.JoyconLeft -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.L, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_zl))
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLLeft,
R.string.button_sl_left
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRLeft,
R.string.button_sr_left
)
)
}
NpadStyleIndex.JoyconRight -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_r))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_zr))
add(
ButtonInputSetting(
playerIndex,
NativeButton.SLRight,
R.string.button_sl_right
)
)
add(
ButtonInputSetting(
playerIndex,
NativeButton.SRRight,
R.string.button_sr_right
)
)
}
NpadStyleIndex.GameCube -> {
add(HeaderSetting(R.string.triggers))
add(ButtonInputSetting(playerIndex, NativeButton.R, R.string.button_z))
add(ButtonInputSetting(playerIndex, NativeButton.ZL, R.string.button_l))
add(ButtonInputSetting(playerIndex, NativeButton.ZR, R.string.button_r))
}
else -> {
// No-op
}
}
add(HeaderSetting(R.string.vibration))
val vibrationEnabledSetting = object : AbstractBooleanSetting {
override val key = "vibration"
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getInputSettings(true)[playerIndex].vibrationEnabled
override fun setBoolean(value: Boolean) {
val settings = NativeConfig.getInputSettings(true)
settings[playerIndex].vibrationEnabled = value
NativeConfig.setInputSettings(settings, true)
}
override val defaultValue = true
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean(needsGlobal).toString()
override fun reset() = setBoolean(defaultValue)
}
add(SwitchSetting(vibrationEnabledSetting, R.string.vibration))
val useSystemVibratorSetting = object : AbstractBooleanSetting {
override val key = ""
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getInputSettings(true)[playerIndex].useSystemVibrator
override fun setBoolean(value: Boolean) {
val settings = NativeConfig.getInputSettings(true)
settings[playerIndex].useSystemVibrator = value
NativeConfig.setInputSettings(settings, true)
}
override val defaultValue = playerIndex == 0
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean(needsGlobal).toString()
override fun reset() = setBoolean(defaultValue)
override val pairedSettingKey: String = "vibration"
}
addAbstract(SwitchSetting(useSystemVibratorSetting, R.string.use_system_vibrator))
val vibrationStrengthSetting = object : AbstractIntSetting {
override val key = ""
override fun getInt(needsGlobal: Boolean): Int =
NativeConfig.getInputSettings(true)[playerIndex].vibrationStrength
override fun setInt(value: Int) {
val settings = NativeConfig.getInputSettings(true)
settings[playerIndex].vibrationStrength = value
NativeConfig.setInputSettings(settings, true)
}
override val defaultValue = 100
override fun getValueAsString(needsGlobal: Boolean): String =
getInt(needsGlobal).toString()
override fun reset() = setInt(defaultValue)
override val pairedSettingKey: String = "vibration"
}
addAbstract(
SliderSetting(vibrationStrengthSetting, R.string.vibration_strength, units = "%")
)
}
}
// Convenience function for creating AbstractIntSettings for modifier range/stick range/stick deadzones
private fun getStickIntSettingFromParam(
playerIndex: Int,
paramName: String,
stick: NativeAnalog,
defaultValue: Int
): AbstractIntSetting =
object : AbstractIntSetting {
val params get() = NativeInput.getStickParam(playerIndex, stick)
override val key = ""
override fun getInt(needsGlobal: Boolean): Int =
(params.get(paramName, 0.15f) * 100).toInt()
override fun setInt(value: Int) {
val tempParams = params
tempParams.set(paramName, value.toFloat() / 100)
NativeInput.setStickParam(playerIndex, stick, tempParams)
}
override val defaultValue = defaultValue
override fun getValueAsString(needsGlobal: Boolean): String =
getInt(needsGlobal).toString()
override fun reset() = setInt(defaultValue)
}
private fun getExtraStickSettings(
playerIndex: Int,
nativeAnalog: NativeAnalog
): List<SettingsItem> {
val stickIsController =
NativeInput.isController(NativeInput.getStickParam(playerIndex, nativeAnalog))
val modifierRangeSetting =
getStickIntSettingFromParam(playerIndex, "modifier_scale", nativeAnalog, 50)
val stickRangeSetting =
getStickIntSettingFromParam(playerIndex, "range", nativeAnalog, 95)
val stickDeadzoneSetting =
getStickIntSettingFromParam(playerIndex, "deadzone", nativeAnalog, 15)
val out = mutableListOf<SettingsItem>().apply {
if (stickIsController) {
add(SliderSetting(stickRangeSetting, titleId = R.string.range, min = 25, max = 150))
add(SliderSetting(stickDeadzoneSetting, R.string.deadzone))
} else {
add(ModifierInputSetting(playerIndex, NativeAnalog.LStick, R.string.modifier))
add(SliderSetting(modifierRangeSetting, R.string.modifier_range))
}
}
return out
}
private fun getStickDirections(player: Int, stick: NativeAnalog): List<AnalogInputSetting> =
listOf(
AnalogInputSetting(
player,
stick,
AnalogDirection.Up,
R.string.up
),
AnalogInputSetting(
player,
stick,
AnalogDirection.Down,
R.string.down
),
AnalogInputSetting(
player,
stick,
AnalogDirection.Left,
R.string.left
),
AnalogInputSetting(
player,
stick,
AnalogDirection.Right,
R.string.right
)
)
private fun addThemeSettings(sl: ArrayList<SettingsItem>) { private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
sl.apply { sl.apply {
val theme: AbstractIntSetting = object : AbstractIntSetting { val theme: AbstractIntSetting = object : AbstractIntSetting {
@ -186,20 +877,18 @@ class SettingsFragmentPresenter(
add( add(
SingleChoiceSetting( SingleChoiceSetting(
theme, theme,
R.string.change_app_theme, titleId = R.string.change_app_theme,
0, choicesId = R.array.themeEntriesA12,
R.array.themeEntriesA12, valuesId = R.array.themeValuesA12
R.array.themeValuesA12
) )
) )
} else { } else {
add( add(
SingleChoiceSetting( SingleChoiceSetting(
theme, theme,
R.string.change_app_theme, titleId = R.string.change_app_theme,
0, choicesId = R.array.themeEntries,
R.array.themeEntries, valuesId = R.array.themeValues
R.array.themeValues
) )
) )
} }
@ -228,10 +917,9 @@ class SettingsFragmentPresenter(
add( add(
SingleChoiceSetting( SingleChoiceSetting(
themeMode, themeMode,
R.string.change_theme_mode, titleId = R.string.change_theme_mode,
0, choicesId = R.array.themeModeEntries,
R.array.themeModeEntries, valuesId = R.array.themeModeValues
R.array.themeModeValues
) )
) )
@ -262,8 +950,8 @@ class SettingsFragmentPresenter(
add( add(
SwitchSetting( SwitchSetting(
blackBackgrounds, blackBackgrounds,
R.string.use_black_backgrounds, titleId = R.string.use_black_backgrounds,
R.string.use_black_backgrounds_description descriptionId = R.string.use_black_backgrounds_description
) )
) )
} }

View File

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments package org.yuzu.yuzu_emu.features.settings.ui
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
@ -26,8 +26,6 @@ import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding
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.model.SettingsViewModel
import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
@ -119,7 +117,7 @@ class SettingsSearchFragment : Fragment() {
val baseList = SettingsItem.settingsItems val baseList = SettingsItem.settingsItems
val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1) val similarityAlgorithm = if (searchTerm.length > 2) Cosine() else Cosine(1)
val sortedList: List<SettingsItem> = baseList.mapNotNull { item -> val sortedList: List<SettingsItem> = baseList.mapNotNull { item ->
val title = getString(item.value.nameId).lowercase() val title = item.value.title.lowercase()
val similarity = similarityAlgorithm.similarity(searchTerm, title) val similarity = similarityAlgorithm.similarity(searchTerm, title)
if (similarity > 0.08) { if (similarity > 0.08) {
Pair(similarity, item) Pair(similarity, item)

View File

@ -1,20 +1,26 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model package org.yuzu.yuzu_emu.features.settings.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
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.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.ParamPackage
class SettingsViewModel : ViewModel() { class SettingsViewModel : ViewModel() {
var game: Game? = null var game: Game? = null
var clickedItem: SettingsItem? = null var clickedItem: SettingsItem? = null
var currentDevice = 0
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
private val _shouldRecreate = MutableStateFlow(false) private val _shouldRecreate = MutableStateFlow(false)
@ -36,6 +42,18 @@ class SettingsViewModel : ViewModel() {
val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged
private val _adapterItemChanged = MutableStateFlow(-1) private val _adapterItemChanged = MutableStateFlow(-1)
private val _datasetChanged = MutableStateFlow(false)
val datasetChanged = _datasetChanged.asStateFlow()
private val _reloadListAndNotifyDataset = MutableStateFlow(false)
val reloadListAndNotifyDataset = _reloadListAndNotifyDataset.asStateFlow()
private val _shouldShowDeleteProfileDialog = MutableStateFlow("")
val shouldShowDeleteProfileDialog = _shouldShowDeleteProfileDialog.asStateFlow()
private val _shouldShowResetInputDialog = MutableStateFlow(false)
val shouldShowResetInputDialog = _shouldShowResetInputDialog.asStateFlow()
fun setShouldRecreate(value: Boolean) { fun setShouldRecreate(value: Boolean) {
_shouldRecreate.value = value _shouldRecreate.value = value
} }
@ -68,4 +86,27 @@ class SettingsViewModel : ViewModel() {
fun setAdapterItemChanged(value: Int) { fun setAdapterItemChanged(value: Int) {
_adapterItemChanged.value = value _adapterItemChanged.value = value
} }
fun setDatasetChanged(value: Boolean) {
_datasetChanged.value = value
}
fun setReloadListAndNotifyDataset(value: Boolean) {
_reloadListAndNotifyDataset.value = value
}
fun setShouldShowDeleteProfileDialog(profile: String) {
_shouldShowDeleteProfileDialog.value = profile
}
fun setShouldShowResetInputDialog(value: Boolean) {
_shouldShowResetInputDialog.value = value
}
fun getCurrentDeviceParams(defaultParams: ParamPackage): ParamPackage =
try {
InputHandler.registeredControllers[currentDevice]
} catch (e: IndexOutOfBoundsException) {
defaultParams
}
} }

View File

@ -21,9 +21,9 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
setting = item as DateTimeSetting setting = item as DateTimeSetting
binding.textSettingName.setText(item.nameId) binding.textSettingName.text = item.title
if (item.descriptionId != 0) { if (setting.description.isNotEmpty()) {
binding.textSettingDescription.setText(item.descriptionId) binding.textSettingDescription.text = item.description
binding.textSettingDescription.visibility = View.VISIBLE binding.textSettingDescription.visibility = View.VISIBLE
} else { } else {
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE

View File

@ -16,7 +16,7 @@ class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: Sett
} }
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
binding.textHeaderName.setText(item.nameId) binding.textHeaderName.text = item.title
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {

View File

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.InputProfileSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.R
class InputProfileViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: InputProfileSetting
override fun bind(item: SettingsItem) {
setting = item as InputProfileSetting
binding.textSettingName.text = setting.title
binding.textSettingValue.text =
setting.getCurrentProfile().ifEmpty { binding.root.context.getString(R.string.not_set) }
binding.textSettingDescription.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
binding.icon.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
}
override fun onClick(clicked: View) =
adapter.onInputProfileClick(setting, bindingAdapterPosition)
override fun onLongClick(clicked: View): Boolean = false
}

View File

@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View
import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.features.settings.model.view.AnalogInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.ButtonInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.InputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.ModifierInputSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
class InputViewHolder(val binding: ListItemSettingInputBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: InputSetting
override fun bind(item: SettingsItem) {
setting = item as InputSetting
binding.textSettingName.text = setting.title
binding.textSettingValue.text = setting.getSelectedValue()
binding.buttonOptions.visibility = when (item) {
is AnalogInputSetting -> {
val param = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
if (
param.get("engine", "") == "analog_from_button" ||
param.has("axis_x") || param.has("axis_y")
) {
View.VISIBLE
} else {
View.GONE
}
}
is ButtonInputSetting -> {
val param = NativeInput.getButtonParam(item.playerIndex, item.nativeButton)
if (
param.has("code") || param.has("button") || param.has("hat") ||
param.has("axis")
) {
View.VISIBLE
} else {
View.GONE
}
}
is ModifierInputSetting -> {
val params = NativeInput.getStickParam(item.playerIndex, item.nativeAnalog)
if (params.has("modifier")) {
View.VISIBLE
} else {
View.GONE
}
}
}
binding.buttonOptions.setOnClickListener(null)
binding.buttonOptions.setOnClickListener {
adapter.onInputOptionsClick(binding.buttonOptions, setting, bindingAdapterPosition)
}
}
override fun onClick(clicked: View) =
adapter.onInputClick(setting, bindingAdapterPosition)
override fun onLongClick(clicked: View): Boolean =
adapter.onLongClick(setting, bindingAdapterPosition)
}

View File

@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View import android.view.View
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting import org.yuzu.yuzu_emu.features.settings.model.view.RunnableSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
@ -17,12 +16,12 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
setting = item as RunnableSetting setting = item as RunnableSetting
if (item.iconId != 0) { if (setting.iconId != 0) {
binding.icon.visibility = View.VISIBLE binding.icon.visibility = View.VISIBLE
binding.icon.setImageDrawable( binding.icon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
binding.icon.resources, binding.icon.resources,
item.iconId, setting.iconId,
binding.icon.context.theme binding.icon.context.theme
) )
) )
@ -30,8 +29,8 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
binding.icon.visibility = View.GONE binding.icon.visibility = View.GONE
} }
binding.textSettingName.setText(item.nameId) binding.textSettingName.text = setting.title
if (item.descriptionId != 0) { if (setting.description.isNotEmpty()) {
binding.textSettingDescription.setText(item.descriptionId) binding.textSettingDescription.setText(item.descriptionId)
binding.textSettingDescription.visibility = View.VISIBLE binding.textSettingDescription.visibility = View.VISIBLE
} else { } else {
@ -44,7 +43,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) { if (setting.isRunnable) {
setting.runnable.invoke() setting.runnable.invoke()
} }
} }

View File

@ -5,6 +5,7 @@ package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View import android.view.View
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.IntSingleChoiceSetting
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.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
@ -17,16 +18,17 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
setting = item setting = item
binding.textSettingName.setText(item.nameId) binding.textSettingName.text = setting.title
if (item.descriptionId != 0) { if (item.description.isNotEmpty()) {
binding.textSettingDescription.setText(item.descriptionId) binding.textSettingDescription.text = item.description
binding.textSettingDescription.visibility = View.VISIBLE binding.textSettingDescription.visibility = View.VISIBLE
} else { } else {
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
binding.textSettingValue.visibility = View.VISIBLE binding.textSettingValue.visibility = View.VISIBLE
if (item is SingleChoiceSetting) { when (item) {
is SingleChoiceSetting -> {
val resMgr = binding.textSettingValue.context.resources val resMgr = binding.textSettingValue.context.resources
val values = resMgr.getIntArray(item.valuesId) val values = resMgr.getIntArray(item.valuesId)
for (i in values.indices) { for (i in values.indices) {
@ -35,13 +37,18 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
break break
} }
} }
} else if (item is StringSingleChoiceSetting) { }
for (i in item.values.indices) {
if (item.values[i] == item.getSelectedValue()) { is StringSingleChoiceSetting -> {
binding.textSettingValue.text = item.choices[i] binding.textSettingValue.text = item.getSelectedValue()
break }
is IntSingleChoiceSetting -> {
binding.textSettingValue.text = item.getChoiceAt(item.getSelectedValue())
} }
} }
if (binding.textSettingValue.text.isEmpty()) {
binding.textSettingValue.visibility = View.GONE
} }
binding.buttonClear.visibility = if (setting.setting.global || binding.buttonClear.visibility = if (setting.setting.global ||
@ -63,17 +70,26 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
return return
} }
if (setting is SingleChoiceSetting) { when (setting) {
adapter.onSingleChoiceClick( is SingleChoiceSetting -> adapter.onSingleChoiceClick(
(setting as SingleChoiceSetting), setting as SingleChoiceSetting,
bindingAdapterPosition bindingAdapterPosition
) )
} else if (setting is StringSingleChoiceSetting) {
is StringSingleChoiceSetting -> {
adapter.onStringSingleChoiceClick( adapter.onStringSingleChoiceClick(
(setting as StringSingleChoiceSetting), setting as StringSingleChoiceSetting,
bindingAdapterPosition bindingAdapterPosition
) )
} }
is IntSingleChoiceSetting -> {
adapter.onIntSingleChoiceClick(
setting as IntSingleChoiceSetting,
bindingAdapterPosition
)
}
}
} }
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {

View File

@ -17,9 +17,9 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
setting = item as SliderSetting setting = item as SliderSetting
binding.textSettingName.setText(item.nameId) binding.textSettingName.text = setting.title
if (item.descriptionId != 0) { if (item.description.isNotEmpty()) {
binding.textSettingDescription.setText(item.descriptionId) binding.textSettingDescription.text = setting.description
binding.textSettingDescription.visibility = View.VISIBLE binding.textSettingDescription.visibility = View.VISIBLE
} else { } else {
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE

View File

@ -12,16 +12,16 @@ import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) { SettingViewHolder(binding.root, adapter) {
private lateinit var item: SubmenuSetting private lateinit var setting: SubmenuSetting
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
this.item = item as SubmenuSetting setting = item as SubmenuSetting
if (item.iconId != 0) { if (setting.iconId != 0) {
binding.icon.visibility = View.VISIBLE binding.icon.visibility = View.VISIBLE
binding.icon.setImageDrawable( binding.icon.setImageDrawable(
ResourcesCompat.getDrawable( ResourcesCompat.getDrawable(
binding.icon.resources, binding.icon.resources,
item.iconId, setting.iconId,
binding.icon.context.theme binding.icon.context.theme
) )
) )
@ -29,9 +29,9 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
binding.icon.visibility = View.GONE binding.icon.visibility = View.GONE
} }
binding.textSettingName.setText(item.nameId) binding.textSettingName.text = setting.title
if (item.descriptionId != 0) { if (setting.description.isNotEmpty()) {
binding.textSettingDescription.setText(item.descriptionId) binding.textSettingDescription.text = setting.description
binding.textSettingDescription.visibility = View.VISIBLE binding.textSettingDescription.visibility = View.VISIBLE
} else { } else {
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
@ -41,7 +41,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
} }
override fun onClick(clicked: View) { override fun onClick(clicked: View) {
adapter.onSubmenuClick(item) adapter.onSubmenuClick(setting)
} }
override fun onLongClick(clicked: View): Boolean { override fun onLongClick(clicked: View): Boolean {

View File

@ -18,19 +18,18 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
override fun bind(item: SettingsItem) { override fun bind(item: SettingsItem) {
setting = item as SwitchSetting setting = item as SwitchSetting
binding.textSettingName.setText(item.nameId) binding.textSettingName.text = setting.title
if (item.descriptionId != 0) { if (setting.description.isNotEmpty()) {
binding.textSettingDescription.setText(item.descriptionId) binding.textSettingDescription.text = setting.description
binding.textSettingDescription.visibility = View.VISIBLE binding.textSettingDescription.visibility = View.VISIBLE
} else { } else {
binding.textSettingDescription.text = ""
binding.textSettingDescription.visibility = View.GONE binding.textSettingDescription.visibility = View.GONE
} }
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, bindingAdapterPosition) adapter.onBooleanClick(setting, binding.switchWidget.isChecked, bindingAdapterPosition)
} }
binding.buttonClear.visibility = if (setting.setting.global || binding.buttonClear.visibility = if (setting.setting.global ||

View File

@ -277,6 +277,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
true true
} }
R.id.menu_controls -> {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
Settings.MenuTag.SECTION_INPUT
)
binding.root.findNavController().navigate(action)
true
}
R.id.menu_overlay_controls -> { R.id.menu_overlay_controls -> {
showOverlayOptions() showOverlayOptions()
true true

View File

@ -89,6 +89,20 @@ class HomeSettingsFragment : Fragment() {
} }
) )
) )
add(
HomeSetting(
R.string.preferences_controls,
R.string.preferences_controls_description,
R.drawable.ic_controller,
{
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
null,
Settings.MenuTag.SECTION_INPUT
)
binding.root.findNavController().navigate(action)
}
)
)
add( add(
HomeSetting( HomeSetting(
R.string.gpu_driver_manager, R.string.gpu_driver_manager,

View File

@ -24,10 +24,10 @@ import androidx.core.content.ContextCompat
import androidx.window.layout.WindowMetricsCalculator import androidx.window.layout.WindowMetricsCalculator
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.NativeLibrary.ButtonType
import org.yuzu.yuzu_emu.NativeLibrary.StickType
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.features.input.model.NativeButton
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.overlay.model.OverlayControl import org.yuzu.yuzu_emu.overlay.model.OverlayControl
@ -100,19 +100,19 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
var shouldUpdateView = false var shouldUpdateView = false
val playerIndex = val playerIndex =
if (NativeLibrary.isHandheldOnly()) { if (NativeInput.isHandheldOnly()) {
NativeLibrary.ConsoleDevice NativeInput.ConsoleDevice
} else { } else {
NativeLibrary.Player1Device NativeInput.Player1Device
} }
for (button in overlayButtons) { for (button in overlayButtons) {
if (!button.updateStatus(event)) { if (!button.updateStatus(event)) {
continue continue
} }
NativeLibrary.onGamePadButtonEvent( NativeInput.onOverlayButtonEvent(
playerIndex, playerIndex,
button.buttonId, button.button,
button.status button.status
) )
playHaptics(event) playHaptics(event)
@ -123,24 +123,24 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) { if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) {
continue continue
} }
NativeLibrary.onGamePadButtonEvent( NativeInput.onOverlayButtonEvent(
playerIndex, playerIndex,
dpad.upId, dpad.up,
dpad.upStatus dpad.upStatus
) )
NativeLibrary.onGamePadButtonEvent( NativeInput.onOverlayButtonEvent(
playerIndex, playerIndex,
dpad.downId, dpad.down,
dpad.downStatus dpad.downStatus
) )
NativeLibrary.onGamePadButtonEvent( NativeInput.onOverlayButtonEvent(
playerIndex, playerIndex,
dpad.leftId, dpad.left,
dpad.leftStatus dpad.leftStatus
) )
NativeLibrary.onGamePadButtonEvent( NativeInput.onOverlayButtonEvent(
playerIndex, playerIndex,
dpad.rightId, dpad.right,
dpad.rightStatus dpad.rightStatus
) )
playHaptics(event) playHaptics(event)
@ -151,16 +151,15 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
if (!joystick.updateStatus(event)) { if (!joystick.updateStatus(event)) {
continue continue
} }
val axisID = joystick.joystickId NativeInput.onOverlayJoystickEvent(
NativeLibrary.onGamePadJoystickEvent(
playerIndex, playerIndex,
axisID, joystick.joystick,
joystick.xAxis, joystick.xAxis,
joystick.realYAxis joystick.realYAxis
) )
NativeLibrary.onGamePadButtonEvent( NativeInput.onOverlayButtonEvent(
playerIndex, playerIndex,
joystick.buttonId, joystick.button,
joystick.buttonStatus joystick.buttonStatus
) )
playHaptics(event) playHaptics(event)
@ -187,7 +186,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
if (isActionDown && !isTouchInputConsumed(pointerId)) { if (isActionDown && !isTouchInputConsumed(pointerId)) {
NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat()) NativeInput.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat())
} }
if (isActionMove) { if (isActionMove) {
@ -196,12 +195,12 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
if (isTouchInputConsumed(fingerId)) { if (isTouchInputConsumed(fingerId)) {
continue continue
} }
NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i)) NativeInput.onTouchMoved(fingerId, event.getX(i), event.getY(i))
} }
} }
if (isActionUp && !isTouchInputConsumed(pointerId)) { if (isActionUp && !isTouchInputConsumed(pointerId)) {
NativeLibrary.onTouchReleased(pointerId) NativeInput.onTouchReleased(pointerId)
} }
return true return true
@ -359,7 +358,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.facebutton_a, R.drawable.facebutton_a,
R.drawable.facebutton_a_depressed, R.drawable.facebutton_a_depressed,
ButtonType.BUTTON_A, NativeButton.A,
data, data,
position position
) )
@ -373,7 +372,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.facebutton_b, R.drawable.facebutton_b,
R.drawable.facebutton_b_depressed, R.drawable.facebutton_b_depressed,
ButtonType.BUTTON_B, NativeButton.B,
data, data,
position position
) )
@ -387,7 +386,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.facebutton_x, R.drawable.facebutton_x,
R.drawable.facebutton_x_depressed, R.drawable.facebutton_x_depressed,
ButtonType.BUTTON_X, NativeButton.X,
data, data,
position position
) )
@ -401,7 +400,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.facebutton_y, R.drawable.facebutton_y,
R.drawable.facebutton_y_depressed, R.drawable.facebutton_y_depressed,
ButtonType.BUTTON_Y, NativeButton.Y,
data, data,
position position
) )
@ -415,7 +414,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.facebutton_plus, R.drawable.facebutton_plus,
R.drawable.facebutton_plus_depressed, R.drawable.facebutton_plus_depressed,
ButtonType.BUTTON_PLUS, NativeButton.Plus,
data, data,
position position
) )
@ -429,7 +428,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.facebutton_minus, R.drawable.facebutton_minus,
R.drawable.facebutton_minus_depressed, R.drawable.facebutton_minus_depressed,
ButtonType.BUTTON_MINUS, NativeButton.Minus,
data, data,
position position
) )
@ -443,7 +442,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.facebutton_home, R.drawable.facebutton_home,
R.drawable.facebutton_home_depressed, R.drawable.facebutton_home_depressed,
ButtonType.BUTTON_HOME, NativeButton.Home,
data, data,
position position
) )
@ -457,7 +456,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.facebutton_screenshot, R.drawable.facebutton_screenshot,
R.drawable.facebutton_screenshot_depressed, R.drawable.facebutton_screenshot_depressed,
ButtonType.BUTTON_CAPTURE, NativeButton.Capture,
data, data,
position position
) )
@ -471,7 +470,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.l_shoulder, R.drawable.l_shoulder,
R.drawable.l_shoulder_depressed, R.drawable.l_shoulder_depressed,
ButtonType.TRIGGER_L, NativeButton.L,
data, data,
position position
) )
@ -485,7 +484,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.r_shoulder, R.drawable.r_shoulder,
R.drawable.r_shoulder_depressed, R.drawable.r_shoulder_depressed,
ButtonType.TRIGGER_R, NativeButton.R,
data, data,
position position
) )
@ -499,7 +498,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.zl_trigger, R.drawable.zl_trigger,
R.drawable.zl_trigger_depressed, R.drawable.zl_trigger_depressed,
ButtonType.TRIGGER_ZL, NativeButton.ZL,
data, data,
position position
) )
@ -513,7 +512,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.zr_trigger, R.drawable.zr_trigger,
R.drawable.zr_trigger_depressed, R.drawable.zr_trigger_depressed,
ButtonType.TRIGGER_ZR, NativeButton.ZR,
data, data,
position position
) )
@ -527,7 +526,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.button_l3, R.drawable.button_l3,
R.drawable.button_l3_depressed, R.drawable.button_l3_depressed,
ButtonType.STICK_L, NativeButton.LStick,
data, data,
position position
) )
@ -541,7 +540,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize, windowSize,
R.drawable.button_r3, R.drawable.button_r3,
R.drawable.button_r3_depressed, R.drawable.button_r3_depressed,
ButtonType.STICK_R, NativeButton.RStick,
data, data,
position position
) )
@ -556,8 +555,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.joystick_range, R.drawable.joystick_range,
R.drawable.joystick, R.drawable.joystick,
R.drawable.joystick_depressed, R.drawable.joystick_depressed,
StickType.STICK_L, NativeAnalog.LStick,
ButtonType.STICK_L, NativeButton.LStick,
data, data,
position position
) )
@ -572,8 +571,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
R.drawable.joystick_range, R.drawable.joystick_range,
R.drawable.joystick, R.drawable.joystick,
R.drawable.joystick_depressed, R.drawable.joystick_depressed,
StickType.STICK_R, NativeAnalog.RStick,
ButtonType.STICK_R, NativeButton.RStick,
data, data,
position position
) )
@ -835,7 +834,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
windowSize: Pair<Point, Point>, windowSize: Pair<Point, Point>,
defaultResId: Int, defaultResId: Int,
pressedResId: Int, pressedResId: Int,
buttonId: Int, button: NativeButton,
overlayControlData: OverlayControlData, overlayControlData: OverlayControlData,
position: Pair<Double, Double> position: Pair<Double, Double>
): InputOverlayDrawableButton { ): InputOverlayDrawableButton {
@ -869,7 +868,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
res, res,
defaultStateBitmap, defaultStateBitmap,
pressedStateBitmap, pressedStateBitmap,
buttonId, button,
overlayControlData overlayControlData
) )
@ -940,11 +939,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
res, res,
defaultStateBitmap, defaultStateBitmap,
pressedOneDirectionStateBitmap, pressedOneDirectionStateBitmap,
pressedTwoDirectionsStateBitmap, pressedTwoDirectionsStateBitmap
ButtonType.DPAD_UP,
ButtonType.DPAD_DOWN,
ButtonType.DPAD_LEFT,
ButtonType.DPAD_RIGHT
) )
// Get the minimum and maximum coordinates of the screen where the button can be placed. // Get the minimum and maximum coordinates of the screen where the button can be placed.
@ -993,8 +988,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
resOuter: Int, resOuter: Int,
defaultResInner: Int, defaultResInner: Int,
pressedResInner: Int, pressedResInner: Int,
joystick: Int, joystick: NativeAnalog,
buttonId: Int, button: NativeButton,
overlayControlData: OverlayControlData, overlayControlData: OverlayControlData,
position: Pair<Double, Double> position: Pair<Double, Double>
): InputOverlayDrawableJoystick { ): InputOverlayDrawableJoystick {
@ -1042,7 +1037,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
outerRect, outerRect,
innerRect, innerRect,
joystick, joystick,
buttonId, button,
overlayControlData.id overlayControlData.id
) )

View File

@ -9,7 +9,8 @@ import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.view.MotionEvent import android.view.MotionEvent
import org.yuzu.yuzu_emu.NativeLibrary.ButtonState import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
import org.yuzu.yuzu_emu.features.input.model.NativeButton
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
/** /**
@ -19,13 +20,13 @@ import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
* @param res [Resources] instance. * @param res [Resources] instance.
* @param defaultStateBitmap [Bitmap] to use with the default state Drawable. * @param defaultStateBitmap [Bitmap] to use with the default state Drawable.
* @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable. * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable.
* @param buttonId Identifier for this type of button. * @param button [NativeButton] for this type of button.
*/ */
class InputOverlayDrawableButton( class InputOverlayDrawableButton(
res: Resources, res: Resources,
defaultStateBitmap: Bitmap, defaultStateBitmap: Bitmap,
pressedStateBitmap: Bitmap, pressedStateBitmap: Bitmap,
val buttonId: Int, val button: NativeButton,
val overlayControlData: OverlayControlData val overlayControlData: OverlayControlData
) { ) {
// The ID value what motion event is tracking // The ID value what motion event is tracking

View File

@ -9,7 +9,8 @@ import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.view.MotionEvent import android.view.MotionEvent
import org.yuzu.yuzu_emu.NativeLibrary.ButtonState import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
import org.yuzu.yuzu_emu.features.input.model.NativeButton
/** /**
* Custom [BitmapDrawable] that is capable * Custom [BitmapDrawable] that is capable
@ -19,20 +20,12 @@ import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
* @param defaultStateBitmap [Bitmap] of the default state. * @param defaultStateBitmap [Bitmap] of the default state.
* @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction. * @param pressedOneDirectionStateBitmap [Bitmap] of the pressed state in one direction.
* @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction. * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction.
* @param buttonUp Identifier for the up button.
* @param buttonDown Identifier for the down button.
* @param buttonLeft Identifier for the left button.
* @param buttonRight Identifier for the right button.
*/ */
class InputOverlayDrawableDpad( class InputOverlayDrawableDpad(
res: Resources, res: Resources,
defaultStateBitmap: Bitmap, defaultStateBitmap: Bitmap,
pressedOneDirectionStateBitmap: Bitmap, pressedOneDirectionStateBitmap: Bitmap,
pressedTwoDirectionsStateBitmap: Bitmap, pressedTwoDirectionsStateBitmap: Bitmap
buttonUp: Int,
buttonDown: Int,
buttonLeft: Int,
buttonRight: Int
) { ) {
/** /**
* Gets one of the InputOverlayDrawableDpad's button IDs. * Gets one of the InputOverlayDrawableDpad's button IDs.
@ -40,10 +33,10 @@ class InputOverlayDrawableDpad(
* @return the requested InputOverlayDrawableDpad's button ID. * @return the requested InputOverlayDrawableDpad's button ID.
*/ */
// The ID identifying what type of button this Drawable represents. // The ID identifying what type of button this Drawable represents.
val upId: Int val up = NativeButton.DUp
val downId: Int val down = NativeButton.DDown
val leftId: Int val left = NativeButton.DLeft
val rightId: Int val right = NativeButton.DRight
var trackId: Int var trackId: Int
val width: Int val width: Int
@ -69,10 +62,6 @@ class InputOverlayDrawableDpad(
this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap) this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap)
width = this.defaultStateBitmap.intrinsicWidth width = this.defaultStateBitmap.intrinsicWidth
height = this.defaultStateBitmap.intrinsicHeight height = this.defaultStateBitmap.intrinsicHeight
upId = buttonUp
downId = buttonDown
leftId = buttonLeft
rightId = buttonRight
trackId = -1 trackId = -1
} }

View File

@ -13,7 +13,9 @@ import kotlin.math.atan2
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.sin import kotlin.math.sin
import kotlin.math.sqrt import kotlin.math.sqrt
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.features.input.NativeInput.ButtonState
import org.yuzu.yuzu_emu.features.input.model.NativeAnalog
import org.yuzu.yuzu_emu.features.input.model.NativeButton
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
/** /**
@ -26,8 +28,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
* @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick. * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick.
* @param rectOuter [Rect] which represents the outer joystick bounds. * @param rectOuter [Rect] which represents the outer joystick bounds.
* @param rectInner [Rect] which represents the inner joystick bounds. * @param rectInner [Rect] which represents the inner joystick bounds.
* @param joystickId The ID value what type of joystick this Drawable represents. * @param joystick The [NativeAnalog] this Drawable represents.
* @param buttonId The ID value what type of button this Drawable represents. * @param button The [NativeButton] this Drawable represents.
*/ */
class InputOverlayDrawableJoystick( class InputOverlayDrawableJoystick(
res: Resources, res: Resources,
@ -36,8 +38,8 @@ class InputOverlayDrawableJoystick(
bitmapInnerPressed: Bitmap, bitmapInnerPressed: Bitmap,
rectOuter: Rect, rectOuter: Rect,
rectInner: Rect, rectInner: Rect,
val joystickId: Int, val joystick: NativeAnalog,
val buttonId: Int, val button: NativeButton,
val prefId: String val prefId: String
) { ) {
// The ID value what motion event is tracking // The ID value what motion event is tracking
@ -69,8 +71,7 @@ class InputOverlayDrawableJoystick(
// TODO: Add button support // TODO: Add button support
val buttonStatus: Int val buttonStatus: Int
get() = get() = ButtonState.RELEASED
NativeLibrary.ButtonState.RELEASED
var bounds: Rect var bounds: Rect
get() = outerBitmap.bounds get() = outerBitmap.bounds
set(bounds) { set(bounds) {

View File

@ -6,439 +6,89 @@ package org.yuzu.yuzu_emu.utils
import android.view.InputDevice import android.view.InputDevice
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
import kotlin.math.sqrt import org.yuzu.yuzu_emu.features.input.NativeInput
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.features.input.YuzuInputOverlayDevice
import org.yuzu.yuzu_emu.features.input.YuzuPhysicalDevice
object InputHandler { object InputHandler {
private var controllerIds = getGameControllerIds() var androidControllers = mapOf<Int, YuzuPhysicalDevice>()
var registeredControllers = mutableListOf<ParamPackage>()
fun initialize() {
// Connect first controller
NativeLibrary.onGamePadConnectEvent(getPlayerNumber(NativeLibrary.Player1Device))
}
fun updateControllerIds() {
controllerIds = getGameControllerIds()
}
fun dispatchKeyEvent(event: KeyEvent): Boolean { fun dispatchKeyEvent(event: KeyEvent): Boolean {
val button: Int = when (event.device.vendorId) {
0x045E -> getInputXboxButtonKey(event.keyCode)
0x054C -> getInputDS5ButtonKey(event.keyCode)
0x057E -> getInputJoyconButtonKey(event.keyCode)
0x1532 -> getInputRazerButtonKey(event.keyCode)
0x3537 -> getInputRedmagicButtonKey(event.keyCode)
0x358A -> getInputBackboneLabsButtonKey(event.keyCode)
else -> getInputGenericButtonKey(event.keyCode)
}
val action = when (event.action) { val action = when (event.action) {
KeyEvent.ACTION_DOWN -> NativeLibrary.ButtonState.PRESSED KeyEvent.ACTION_DOWN -> NativeInput.ButtonState.PRESSED
KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED KeyEvent.ACTION_UP -> NativeInput.ButtonState.RELEASED
else -> return false else -> return false
} }
// Ignore invalid buttons var controllerData = androidControllers[event.device.controllerNumber]
if (button < 0) { if (controllerData == null) {
return false updateControllerData()
controllerData = androidControllers[event.device.controllerNumber] ?: return false
} }
return NativeLibrary.onGamePadButtonEvent( NativeInput.onGamePadButtonEvent(
getPlayerNumber(event.device.controllerNumber, event.deviceId), controllerData.getGUID(),
button, controllerData.getPort(),
event.keyCode,
action action
) )
}
fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
val device = event.device
// Check every axis input available on the controller
for (range in device.motionRanges) {
val axis = range.axis
when (device.vendorId) {
0x045E -> setGenericAxisInput(event, axis)
0x054C -> setGenericAxisInput(event, axis)
0x057E -> setJoyconAxisInput(event, axis)
0x1532 -> setRazerAxisInput(event, axis)
else -> setGenericAxisInput(event, axis)
}
}
return true return true
} }
private fun getPlayerNumber(index: Int, deviceId: Int = -1): Int { fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
var deviceIndex = index val controllerData =
if (deviceId != -1) { androidControllers[event.device.controllerNumber] ?: return false
deviceIndex = controllerIds[deviceId] ?: 0 event.device.motionRanges.forEach {
} NativeInput.onGamePadAxisEvent(
controllerData.getGUID(),
// TODO: Joycons are handled as different controllers. Find a way to merge them. controllerData.getPort(),
return when (deviceIndex) { it.axis,
2 -> NativeLibrary.Player2Device event.getAxisValue(it.axis)
3 -> NativeLibrary.Player3Device
4 -> NativeLibrary.Player4Device
5 -> NativeLibrary.Player5Device
6 -> NativeLibrary.Player6Device
7 -> NativeLibrary.Player7Device
8 -> NativeLibrary.Player8Device
else -> if (NativeLibrary.isHandheldOnly()) {
NativeLibrary.ConsoleDevice
} else {
NativeLibrary.Player1Device
}
}
}
private fun setStickState(playerNumber: Int, index: Int, xAxis: Float, yAxis: Float) {
// Calculate vector size
val r2 = xAxis * xAxis + yAxis * yAxis
var r = sqrt(r2.toDouble()).toFloat()
// Adjust range of joystick
val deadzone = 0.15f
var x = xAxis
var y = yAxis
if (r > deadzone) {
val deadzoneFactor = 1.0f / r * (r - deadzone) / (1.0f - deadzone)
x *= deadzoneFactor
y *= deadzoneFactor
r *= deadzoneFactor
} else {
x = 0.0f
y = 0.0f
}
// Normalize joystick
if (r > 1.0f) {
x /= r
y /= r
}
NativeLibrary.onGamePadJoystickEvent(
playerNumber,
index,
x,
-y
) )
} }
return true
private fun getAxisToButton(axis: Float): Int {
return if (axis > 0.5f) {
NativeLibrary.ButtonState.PRESSED
} else {
NativeLibrary.ButtonState.RELEASED
}
} }
private fun setAxisDpadState(playerNumber: Int, xAxis: Float, yAxis: Float) { fun getDevices(): Map<Int, YuzuPhysicalDevice> {
NativeLibrary.onGamePadButtonEvent( val gameControllerDeviceIds = mutableMapOf<Int, YuzuPhysicalDevice>()
playerNumber,
NativeLibrary.ButtonType.DPAD_UP,
getAxisToButton(-yAxis)
)
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.DPAD_DOWN,
getAxisToButton(yAxis)
)
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.DPAD_LEFT,
getAxisToButton(-xAxis)
)
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.DPAD_RIGHT,
getAxisToButton(xAxis)
)
}
private fun getInputDS5ButtonKey(key: Int): Int {
// The missing ds5 buttons are axis
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputJoyconButtonKey(key: Int): Int {
// Joycon support is half dead. A lot of buttons can't be mapped
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputXboxButtonKey(key: Int): Int {
// The missing xbox buttons are axis
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputRazerButtonKey(key: Int): Int {
// The missing xbox buttons are axis
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputRedmagicButtonKey(key: Int): Int {
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputBackboneLabsButtonKey(key: Int): Int {
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun getInputGenericButtonKey(key: Int): Int {
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
KeyEvent.KEYCODE_DPAD_UP -> NativeLibrary.ButtonType.DPAD_UP
KeyEvent.KEYCODE_DPAD_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
KeyEvent.KEYCODE_DPAD_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
KeyEvent.KEYCODE_DPAD_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
else -> -1
}
}
private fun setGenericAxisInput(event: MotionEvent, axis: Int) {
val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
when (axis) {
MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_L,
event.getAxisValue(MotionEvent.AXIS_X),
event.getAxisValue(MotionEvent.AXIS_Y)
)
MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_R,
event.getAxisValue(MotionEvent.AXIS_RX),
event.getAxisValue(MotionEvent.AXIS_RY)
)
MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_R,
event.getAxisValue(MotionEvent.AXIS_Z),
event.getAxisValue(MotionEvent.AXIS_RZ)
)
MotionEvent.AXIS_LTRIGGER ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZL,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_LTRIGGER))
)
MotionEvent.AXIS_BRAKE ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZL,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
)
MotionEvent.AXIS_RTRIGGER ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZR,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_RTRIGGER))
)
MotionEvent.AXIS_GAS ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZR,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
)
MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
setAxisDpadState(
playerNumber,
event.getAxisValue(MotionEvent.AXIS_HAT_X),
event.getAxisValue(MotionEvent.AXIS_HAT_Y)
)
}
}
private fun setJoyconAxisInput(event: MotionEvent, axis: Int) {
// Joycon support is half dead. Right joystick doesn't work
val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
when (axis) {
MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_L,
event.getAxisValue(MotionEvent.AXIS_X),
event.getAxisValue(MotionEvent.AXIS_Y)
)
MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_R,
event.getAxisValue(MotionEvent.AXIS_Z),
event.getAxisValue(MotionEvent.AXIS_RZ)
)
MotionEvent.AXIS_RX, MotionEvent.AXIS_RY ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_R,
event.getAxisValue(MotionEvent.AXIS_RX),
event.getAxisValue(MotionEvent.AXIS_RY)
)
}
}
private fun setRazerAxisInput(event: MotionEvent, axis: Int) {
val playerNumber = getPlayerNumber(event.device.controllerNumber, event.deviceId)
when (axis) {
MotionEvent.AXIS_X, MotionEvent.AXIS_Y ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_L,
event.getAxisValue(MotionEvent.AXIS_X),
event.getAxisValue(MotionEvent.AXIS_Y)
)
MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ ->
setStickState(
playerNumber,
NativeLibrary.StickType.STICK_R,
event.getAxisValue(MotionEvent.AXIS_Z),
event.getAxisValue(MotionEvent.AXIS_RZ)
)
MotionEvent.AXIS_BRAKE ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZL,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_BRAKE))
)
MotionEvent.AXIS_GAS ->
NativeLibrary.onGamePadButtonEvent(
playerNumber,
NativeLibrary.ButtonType.TRIGGER_ZR,
getAxisToButton(event.getAxisValue(MotionEvent.AXIS_GAS))
)
MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y ->
setAxisDpadState(
playerNumber,
event.getAxisValue(MotionEvent.AXIS_HAT_X),
event.getAxisValue(MotionEvent.AXIS_HAT_Y)
)
}
}
fun getGameControllerIds(): Map<Int, Int> {
val gameControllerDeviceIds = mutableMapOf<Int, Int>()
val deviceIds = InputDevice.getDeviceIds() val deviceIds = InputDevice.getDeviceIds()
var controllerSlot = 1 var port = 0
val inputSettings = NativeConfig.getInputSettings(true)
deviceIds.forEach { deviceId -> deviceIds.forEach { deviceId ->
InputDevice.getDevice(deviceId)?.apply { InputDevice.getDevice(deviceId)?.apply {
// Don't over-assign controllers
if (controllerSlot >= 8) {
return gameControllerDeviceIds
}
// Verify that the device has gamepad buttons, control sticks, or both. // Verify that the device has gamepad buttons, control sticks, or both.
if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK
) { ) {
// This device is a game controller. Store its device ID. if (!gameControllerDeviceIds.contains(controllerNumber)) {
if (deviceId and id and vendorId and productId != 0) { gameControllerDeviceIds[controllerNumber] = YuzuPhysicalDevice(
// Additionally filter out devices that have no ID this,
gameControllerDeviceIds port,
.takeIf { !it.contains(deviceId) } inputSettings[port].useSystemVibrator
?.put(deviceId, controllerSlot) )
controllerSlot++
} }
port++
} }
} }
} }
return gameControllerDeviceIds return gameControllerDeviceIds
} }
fun updateControllerData() {
androidControllers = getDevices()
androidControllers.forEach {
NativeInput.registerController(it.value)
}
// Register the input overlay on a dedicated port for all player 1 vibrations
NativeInput.registerController(YuzuInputOverlayDevice(androidControllers.isEmpty(), 100))
registeredControllers.clear()
NativeInput.getInputDevices().forEach {
registeredControllers.add(ParamPackage(it))
}
registeredControllers.sortBy { it.get("port", 0) }
}
fun InputDevice.getGUID(): String = String.format("%016x%016x", productId, vendorId)
} }

View File

@ -6,6 +6,8 @@ package org.yuzu.yuzu_emu.utils
import org.yuzu.yuzu_emu.model.GameDir import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
import org.yuzu.yuzu_emu.features.input.model.PlayerInput
object NativeConfig { object NativeConfig {
/** /**
* Loads global config. * Loads global config.
@ -168,4 +170,17 @@ object NativeConfig {
*/ */
@Synchronized @Synchronized
external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>) external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>)
@Synchronized
external fun getInputSettings(global: Boolean): Array<PlayerInput>
@Synchronized
external fun setInputSettings(value: Array<PlayerInput>, global: Boolean)
/**
* Saves control values for a specific player
* Must be used when per game config is loaded
*/
@Synchronized
external fun saveControlPlayerValues()
} }

View File

@ -14,7 +14,7 @@ import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import java.io.IOException import java.io.IOException
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.features.input.NativeInput
class NfcReader(private val activity: Activity) { class NfcReader(private val activity: Activity) {
private var nfcAdapter: NfcAdapter? = null private var nfcAdapter: NfcAdapter? = null
@ -76,12 +76,12 @@ class NfcReader(private val activity: Activity) {
amiibo.connect() amiibo.connect()
val tagData = ntag215ReadAll(amiibo) ?: return val tagData = ntag215ReadAll(amiibo) ?: return
NativeLibrary.onReadNfcTag(tagData) NativeInput.onReadNfcTag(tagData)
nfcAdapter?.ignore( nfcAdapter?.ignore(
tag, tag,
1000, 1000,
{ NativeLibrary.onRemoveNfcTag() }, { NativeInput.onRemoveNfcTag() },
Handler(Looper.getMainLooper()) Handler(Looper.getMainLooper())
) )
} }

View File

@ -0,0 +1,141 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
// Kotlin version of src/common/param_package.h
class ParamPackage(serialized: String = "") {
private val KEY_VALUE_SEPARATOR = ":"
private val PARAM_SEPARATOR = ","
private val ESCAPE_CHARACTER = "$"
private val KEY_VALUE_SEPARATOR_ESCAPE = "$0"
private val PARAM_SEPARATOR_ESCAPE = "$1"
private val ESCAPE_CHARACTER_ESCAPE = "$2"
private val EMPTY_PLACEHOLDER = "[empty]"
val data = mutableMapOf<String, String>()
init {
val pairs = serialized.split(PARAM_SEPARATOR)
for (pair in pairs) {
val keyValue = pair.split(KEY_VALUE_SEPARATOR).toMutableList()
if (keyValue.size != 2) {
Log.error("[ParamPackage] Invalid key pair $keyValue")
continue
}
keyValue.forEachIndexed { i: Int, _: String ->
keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR_ESCAPE, KEY_VALUE_SEPARATOR)
keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR_ESCAPE, PARAM_SEPARATOR)
keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER_ESCAPE, ESCAPE_CHARACTER)
}
set(keyValue[0], keyValue[1])
}
}
constructor(params: List<Pair<String, String>>) : this() {
params.forEach {
data[it.first] = it.second
}
}
fun serialize(): String {
if (data.isEmpty()) {
return EMPTY_PLACEHOLDER
}
val result = StringBuilder()
data.forEach {
val keyValue = mutableListOf(it.key, it.value)
keyValue.forEachIndexed { i, _ ->
keyValue[i] = keyValue[i].replace(ESCAPE_CHARACTER, ESCAPE_CHARACTER_ESCAPE)
keyValue[i] = keyValue[i].replace(PARAM_SEPARATOR, PARAM_SEPARATOR_ESCAPE)
keyValue[i] = keyValue[i].replace(KEY_VALUE_SEPARATOR, KEY_VALUE_SEPARATOR_ESCAPE)
}
result.append("${keyValue[0]}$KEY_VALUE_SEPARATOR${keyValue[1]}$PARAM_SEPARATOR")
}
return result.removeSuffix(PARAM_SEPARATOR).toString()
}
fun get(key: String, defaultValue: String): String =
if (has(key)) {
data[key]!!
} else {
Log.debug("[ParamPackage] key $key not found")
defaultValue
}
fun get(key: String, defaultValue: Int): Int =
if (has(key)) {
try {
data[key]!!.toInt()
} catch (e: NumberFormatException) {
Log.debug("[ParamPackage] failed to convert ${data[key]!!} to int")
defaultValue
}
} else {
Log.debug("[ParamPackage] key $key not found")
defaultValue
}
private fun Int.toBoolean(): Boolean =
if (this == 1) {
true
} else if (this == 0) {
false
} else {
throw Exception("Tried to convert a value to a boolean that was not 0 or 1!")
}
fun get(key: String, defaultValue: Boolean): Boolean =
if (has(key)) {
try {
get(key, if (defaultValue) 1 else 0).toBoolean()
} catch (e: Exception) {
Log.debug("[ParamPackage] failed to convert ${data[key]!!} to boolean")
defaultValue
}
} else {
Log.debug("[ParamPackage] key $key not found")
defaultValue
}
fun get(key: String, defaultValue: Float): Float =
if (has(key)) {
try {
data[key]!!.toFloat()
} catch (e: NumberFormatException) {
Log.debug("[ParamPackage] failed to convert ${data[key]!!} to float")
defaultValue
}
} else {
Log.debug("[ParamPackage] key $key not found")
defaultValue
}
fun set(key: String, value: String) {
data[key] = value
}
fun set(key: String, value: Int) {
data[key] = value.toString()
}
fun Boolean.toInt(): Int = if (this) 1 else 0
fun set(key: String, value: Boolean) {
data[key] = value.toInt().toString()
}
fun set(key: String, value: Float) {
data[key] = value.toString()
}
fun has(key: String): Boolean = data.containsKey(key)
fun erase(key: String) = data.remove(key)
fun clear() = data.clear()
}

View File

@ -12,6 +12,7 @@ add_library(yuzu-android SHARED
native_log.cpp native_log.cpp
android_config.cpp android_config.cpp
android_config.h android_config.h
native_input.cpp
) )
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR}) set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})

View File

@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <common/logging/log.h>
#include <input_common/main.h>
#include "android_config.h" #include "android_config.h"
#include "android_settings.h" #include "android_settings.h"
#include "common/settings_setting.h" #include "common/settings_setting.h"
@ -32,6 +34,7 @@ void AndroidConfig::ReadAndroidValues() {
ReadOverlayValues(); ReadOverlayValues();
} }
ReadDriverValues(); ReadDriverValues();
ReadAndroidControlValues();
} }
void AndroidConfig::ReadAndroidUIValues() { void AndroidConfig::ReadAndroidUIValues() {
@ -107,6 +110,76 @@ void AndroidConfig::ReadOverlayValues() {
EndGroup(); EndGroup();
} }
void AndroidConfig::ReadAndroidPlayerValues(std::size_t player_index) {
std::string player_prefix;
if (type != ConfigType::InputProfile) {
player_prefix.append("player_").append(ToString(player_index)).append("_");
}
auto& player = Settings::values.players.GetValue()[player_index];
if (IsCustomConfig()) {
const auto profile_name =
ReadStringSetting(std::string(player_prefix).append("profile_name"));
if (profile_name.empty()) {
// Use the global input config
player = Settings::values.players.GetValue(true)[player_index];
player.profile_name = "";
return;
}
}
// Android doesn't have default options for controllers. We have the input overlay for that.
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param;
auto& player_buttons = player.buttons[i];
player_buttons = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeButton::mapping[i]), default_param);
if (player_buttons.empty()) {
player_buttons = default_param;
}
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param;
auto& player_analogs = player.analogs[i];
player_analogs = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), default_param);
if (player_analogs.empty()) {
player_analogs = default_param;
}
}
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
const std::string default_param;
auto& player_motions = player.motions[i];
player_motions = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), default_param);
if (player_motions.empty()) {
player_motions = default_param;
}
}
player.use_system_vibrator = ReadBooleanSetting(
std::string(player_prefix).append("use_system_vibrator"), player_index == 0);
}
void AndroidConfig::ReadAndroidControlValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
Settings::values.players.SetGlobal(!IsCustomConfig());
for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
ReadAndroidPlayerValues(p);
}
if (IsCustomConfig()) {
EndGroup();
return;
}
// ReadDebugControlValues();
// ReadHidbusValues();
EndGroup();
}
void AndroidConfig::SaveAndroidValues() { void AndroidConfig::SaveAndroidValues() {
if (global) { if (global) {
SaveAndroidUIValues(); SaveAndroidUIValues();
@ -114,6 +187,7 @@ void AndroidConfig::SaveAndroidValues() {
SaveOverlayValues(); SaveOverlayValues();
} }
SaveDriverValues(); SaveDriverValues();
SaveAndroidControlValues();
WriteToIni(); WriteToIni();
} }
@ -187,6 +261,52 @@ void AndroidConfig::SaveOverlayValues() {
EndGroup(); EndGroup();
} }
void AndroidConfig::SaveAndroidPlayerValues(std::size_t player_index) {
std::string player_prefix;
if (type != ConfigType::InputProfile) {
player_prefix = std::string("player_").append(ToString(player_index)).append("_");
}
const auto& player = Settings::values.players.GetValue()[player_index];
if (IsCustomConfig() && player.profile_name.empty()) {
// No custom profile selected
return;
}
const std::string default_param;
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
WriteStringSetting(std::string(player_prefix).append(Settings::NativeButton::mapping[i]),
player.buttons[i], std::make_optional(default_param));
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
WriteStringSetting(std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]),
player.analogs[i], std::make_optional(default_param));
}
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
WriteStringSetting(std::string(player_prefix).append(Settings::NativeMotion::mapping[i]),
player.motions[i], std::make_optional(default_param));
}
WriteBooleanSetting(std::string(player_prefix).append("use_system_vibrator"),
player.use_system_vibrator, std::make_optional(player_index == 0));
}
void AndroidConfig::SaveAndroidControlValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
Settings::values.players.SetGlobal(!IsCustomConfig());
for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
SaveAndroidPlayerValues(p);
}
if (IsCustomConfig()) {
EndGroup();
return;
}
// SaveDebugControlValues();
// SaveHidbusValues();
EndGroup();
}
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
auto& map = Settings::values.linkage.by_category; auto& map = Settings::values.linkage.by_category;
if (map.contains(category)) { if (map.contains(category)) {
@ -194,3 +314,24 @@ std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::
} }
return AndroidSettings::values.linkage.by_category[category]; return AndroidSettings::values.linkage.by_category[category];
} }
void AndroidConfig::ReadAndroidControlPlayerValues(std::size_t player_index) {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
ReadPlayerValues(player_index);
ReadAndroidPlayerValues(player_index);
EndGroup();
}
void AndroidConfig::SaveAndroidControlPlayerValues(std::size_t player_index) {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
LOG_DEBUG(Config, "Saving players control configuration values");
SavePlayerValues(player_index);
SaveAndroidPlayerValues(player_index);
EndGroup();
WriteToIni();
}

View File

@ -13,7 +13,12 @@ public:
void ReloadAllValues() override; void ReloadAllValues() override;
void SaveAllValues() override; void SaveAllValues() override;
void ReadAndroidControlPlayerValues(std::size_t player_index);
void SaveAndroidControlPlayerValues(std::size_t player_index);
protected: protected:
void ReadAndroidPlayerValues(std::size_t player_index);
void ReadAndroidControlValues();
void ReadAndroidValues(); void ReadAndroidValues();
void ReadAndroidUIValues(); void ReadAndroidUIValues();
void ReadDriverValues(); void ReadDriverValues();
@ -27,6 +32,8 @@ protected:
void ReadUILayoutValues() override {} void ReadUILayoutValues() override {}
void ReadMultiplayerValues() override {} void ReadMultiplayerValues() override {}
void SaveAndroidPlayerValues(std::size_t player_index);
void SaveAndroidControlValues();
void SaveAndroidValues(); void SaveAndroidValues();
void SaveAndroidUIValues(); void SaveAndroidUIValues();
void SaveDriverValues(); void SaveDriverValues();

View File

@ -5,6 +5,7 @@
#include "common/android/id_cache.h" #include "common/android/id_cache.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "input_common/drivers/android.h"
#include "input_common/drivers/touch_screen.h" #include "input_common/drivers/touch_screen.h"
#include "input_common/drivers/virtual_amiibo.h" #include "input_common/drivers/virtual_amiibo.h"
#include "input_common/drivers/virtual_gamepad.h" #include "input_common/drivers/virtual_gamepad.h"
@ -22,43 +23,6 @@ void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
window_info.render_surface = reinterpret_cast<void*>(surface); window_info.render_surface = reinterpret_cast<void*>(surface);
} }
void EmuWindow_Android::OnTouchPressed(int id, float x, float y) {
const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
m_input_subsystem->GetTouchScreen()->TouchPressed(touch_x, touch_y, id);
}
void EmuWindow_Android::OnTouchMoved(int id, float x, float y) {
const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
m_input_subsystem->GetTouchScreen()->TouchMoved(touch_x, touch_y, id);
}
void EmuWindow_Android::OnTouchReleased(int id) {
m_input_subsystem->GetTouchScreen()->TouchReleased(id);
}
void EmuWindow_Android::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) {
m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed);
}
void EmuWindow_Android::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) {
m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y);
}
void EmuWindow_Android::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x,
float gyro_y, float gyro_z, float accel_x,
float accel_y, float accel_z) {
m_input_subsystem->GetVirtualGamepad()->SetMotionState(
player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
}
void EmuWindow_Android::OnReadNfcTag(std::span<u8> data) {
m_input_subsystem->GetVirtualAmiibo()->LoadAmiibo(data);
}
void EmuWindow_Android::OnRemoveNfcTag() {
m_input_subsystem->GetVirtualAmiibo()->CloseAmiibo();
}
void EmuWindow_Android::OnFrameDisplayed() { void EmuWindow_Android::OnFrameDisplayed() {
if (!m_first_frame) { if (!m_first_frame) {
Common::Android::RunJNIOnFiber<void>( Common::Android::RunJNIOnFiber<void>(
@ -67,10 +31,9 @@ void EmuWindow_Android::OnFrameDisplayed() {
} }
} }
EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface,
ANativeWindow* surface,
std::shared_ptr<Common::DynamicLibrary> driver_library) std::shared_ptr<Common::DynamicLibrary> driver_library)
: m_input_subsystem{input_subsystem}, m_driver_library{driver_library} { : m_driver_library{driver_library} {
LOG_INFO(Frontend, "initializing"); LOG_INFO(Frontend, "initializing");
if (!surface) { if (!surface) {
@ -80,10 +43,4 @@ EmuWindow_Android::EmuWindow_Android(InputCommon::InputSubsystem* input_subsyste
OnSurfaceChanged(surface); OnSurfaceChanged(surface);
window_info.type = Core::Frontend::WindowSystemType::Android; window_info.type = Core::Frontend::WindowSystemType::Android;
m_input_subsystem->Initialize();
}
EmuWindow_Android::~EmuWindow_Android() {
m_input_subsystem->Shutdown();
} }

View File

@ -30,21 +30,12 @@ private:
class EmuWindow_Android final : public Core::Frontend::EmuWindow { class EmuWindow_Android final : public Core::Frontend::EmuWindow {
public: public:
EmuWindow_Android(InputCommon::InputSubsystem* input_subsystem, ANativeWindow* surface, EmuWindow_Android(ANativeWindow* surface,
std::shared_ptr<Common::DynamicLibrary> driver_library); std::shared_ptr<Common::DynamicLibrary> driver_library);
~EmuWindow_Android(); ~EmuWindow_Android() = default;
void OnSurfaceChanged(ANativeWindow* surface); void OnSurfaceChanged(ANativeWindow* surface);
void OnTouchPressed(int id, float x, float y);
void OnTouchMoved(int id, float x, float y);
void OnTouchReleased(int id);
void OnGamepadButtonEvent(int player_index, int button_id, bool pressed);
void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y);
void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y,
float gyro_z, float accel_x, float accel_y, float accel_z);
void OnReadNfcTag(std::span<u8> data);
void OnRemoveNfcTag();
void OnFrameDisplayed() override; void OnFrameDisplayed() override;
std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override { std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {
@ -55,8 +46,6 @@ public:
}; };
private: private:
InputCommon::InputSubsystem* m_input_subsystem{};
float m_window_width{}; float m_window_width{};
float m_window_height{}; float m_window_height{};

View File

@ -88,6 +88,10 @@ FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {
return m_manual_provider.get(); return m_manual_provider.get();
} }
InputCommon::InputSubsystem& EmulationSession::GetInputSubsystem() {
return m_input_subsystem;
}
const EmuWindow_Android& EmulationSession::Window() const { const EmuWindow_Android& EmulationSession::Window() const {
return *m_window; return *m_window;
} }
@ -198,6 +202,8 @@ void EmulationSession::InitializeSystem(bool reload) {
Common::Log::Initialize(); Common::Log::Initialize();
Common::Log::SetColorConsoleBackendEnabled(true); Common::Log::SetColorConsoleBackendEnabled(true);
Common::Log::Start(); Common::Log::Start();
m_input_subsystem.Initialize();
} }
// Initialize filesystem. // Initialize filesystem.
@ -222,8 +228,7 @@ Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string
std::scoped_lock lock(m_mutex); std::scoped_lock lock(m_mutex);
// Create the render window. // Create the render window.
m_window = m_window = std::make_unique<EmuWindow_Android>(m_native_window, m_vulkan_library);
std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library);
// Initialize system. // Initialize system.
jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>(); jauto android_keyboard = std::make_unique<Common::Android::SoftwareKeyboard::AndroidKeyboard>();
@ -355,60 +360,6 @@ void EmulationSession::RunEmulation() {
m_applet_id = static_cast<int>(Service::AM::AppletId::Application); m_applet_id = static_cast<int>(Service::AM::AppletId::Application);
} }
bool EmulationSession::IsHandheldOnly() {
jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag();
if (npad_style_set.fullkey == 1) {
return false;
}
if (npad_style_set.handheld == 0) {
return false;
}
return !Settings::IsDockedMode();
}
void EmulationSession::SetDeviceType([[maybe_unused]] int index, int type) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type));
}
void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
// Ensure that player1 is configured correctly and handheld disconnected
if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) {
jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
handheld->Disconnect();
}
}
// Ensure that handheld is configured correctly and player 1 disconnected
if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) {
jauto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) {
player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
player1->Disconnect();
}
}
if (!controller->IsConnected()) {
controller->Connect();
}
}
void EmulationSession::OnGamepadDisconnectEvent([[maybe_unused]] int index) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
controller->Disconnect();
}
Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() { Common::Android::SoftwareKeyboard::AndroidKeyboard* EmulationSession::SoftwareKeyboard() {
return m_software_keyboard; return m_software_keyboard;
} }
@ -574,14 +525,14 @@ jobjectArray Java_org_yuzu_yuzu_1emu_utils_GpuDriverHelper_getSystemDriverInfo(
nullptr, nullptr, file_redirect_dir_, nullptr); nullptr, nullptr, file_redirect_dir_, nullptr);
auto driver_library = std::make_shared<Common::DynamicLibrary>(handle); auto driver_library = std::make_shared<Common::DynamicLibrary>(handle);
InputCommon::InputSubsystem input_subsystem; InputCommon::InputSubsystem input_subsystem;
auto m_window = std::make_unique<EmuWindow_Android>( auto window =
&input_subsystem, ANativeWindow_fromSurface(env, j_surf), driver_library); std::make_unique<EmuWindow_Android>(ANativeWindow_fromSurface(env, j_surf), driver_library);
Vulkan::vk::InstanceDispatch dld; Vulkan::vk::InstanceDispatch dld;
Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance( Vulkan::vk::Instance vk_instance = Vulkan::CreateInstance(
*driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android); *driver_library, dld, VK_API_VERSION_1_1, Core::Frontend::WindowSystemType::Android);
auto surface = Vulkan::CreateSurface(vk_instance, m_window->GetWindowInfo()); auto surface = Vulkan::CreateSurface(vk_instance, window->GetWindowInfo());
auto device = Vulkan::CreateDevice(vk_instance, dld, *surface); auto device = Vulkan::CreateDevice(vk_instance, dld, *surface);
@ -622,103 +573,6 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz
return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused()); return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
} }
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly(JNIEnv* env, jclass clazz) {
return EmulationSession::GetInstance().IsHandheldOnly();
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_setDeviceType(JNIEnv* env, jclass clazz,
jint j_device, jint j_type) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().SetDeviceType(j_device, j_type);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadConnectEvent(JNIEnv* env, jclass clazz,
jint j_device) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadDisconnectEvent(JNIEnv* env, jclass clazz,
jint j_device) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().OnGamepadDisconnectEvent(j_device);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadButtonEvent(JNIEnv* env, jclass clazz,
jint j_device, jint j_button,
jint action) {
if (EmulationSession::GetInstance().IsRunning()) {
// Ensure gamepad is connected
EmulationSession::GetInstance().OnGamepadConnectEvent(j_device);
EmulationSession::GetInstance().Window().OnGamepadButtonEvent(j_device, j_button,
action != 0);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadJoystickEvent(JNIEnv* env, jclass clazz,
jint j_device, jint stick_id,
jfloat x, jfloat y) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(j_device, stick_id, x, y);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadMotionEvent(
JNIEnv* env, jclass clazz, jint j_device, jlong delta_timestamp, jfloat gyro_x, jfloat gyro_y,
jfloat gyro_z, jfloat accel_x, jfloat accel_y, jfloat accel_z) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnGamepadMotionEvent(
j_device, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onReadNfcTag(JNIEnv* env, jclass clazz,
jbyteArray j_data) {
jboolean isCopy{false};
std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
static_cast<size_t>(env->GetArrayLength(j_data)));
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnReadNfcTag(data);
}
return static_cast<jboolean>(true);
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_onRemoveNfcTag(JNIEnv* env, jclass clazz) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnRemoveNfcTag();
}
return static_cast<jboolean>(true);
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchPressed(JNIEnv* env, jclass clazz, jint id,
jfloat x, jfloat y) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnTouchPressed(id, x, y);
}
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz, jint id,
jfloat x, jfloat y) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnTouchMoved(id, x, y);
}
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchReleased(JNIEnv* env, jclass clazz, jint id) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().Window().OnTouchReleased(id);
}
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz, void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,
jboolean reload) { jboolean reload) {
// Initialize the emulated system. // Initialize the emulated system.
@ -759,6 +613,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject
void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) { void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
EmulationSession::GetInstance().System().ApplySettings(); EmulationSession::GetInstance().System().ApplySettings();
EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) { void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) {

View File

@ -23,6 +23,7 @@ public:
const Core::System& System() const; const Core::System& System() const;
Core::System& System(); Core::System& System();
FileSys::ManualContentProvider* GetContentProvider(); FileSys::ManualContentProvider* GetContentProvider();
InputCommon::InputSubsystem& GetInputSubsystem();
const EmuWindow_Android& Window() const; const EmuWindow_Android& Window() const;
EmuWindow_Android& Window(); EmuWindow_Android& Window();
@ -50,10 +51,6 @@ public:
const std::size_t program_index, const std::size_t program_index,
const bool frontend_initiated); const bool frontend_initiated);
bool IsHandheldOnly();
void SetDeviceType([[maybe_unused]] int index, int type);
void OnGamepadConnectEvent([[maybe_unused]] int index);
void OnGamepadDisconnectEvent([[maybe_unused]] int index);
Common::Android::SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard(); Common::Android::SoftwareKeyboard::AndroidKeyboard* SoftwareKeyboard();
static void OnEmulationStarted(); static void OnEmulationStarted();

View File

@ -3,7 +3,6 @@
#include <string> #include <string>
#include <common/fs/fs_util.h>
#include <jni.h> #include <jni.h>
#include "android_config.h" #include "android_config.h"
@ -425,4 +424,120 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setOverlayControlData(
} }
} }
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInputSettings(JNIEnv* env, jobject obj,
jboolean j_global) {
Settings::values.players.SetGlobal(static_cast<bool>(j_global));
auto& players = Settings::values.players.GetValue();
jobjectArray j_input_settings =
env->NewObjectArray(players.size(), Common::Android::GetPlayerInputClass(), nullptr);
for (size_t i = 0; i < players.size(); ++i) {
auto j_connected = static_cast<jboolean>(players[i].connected);
jobjectArray j_buttons = env->NewObjectArray(
players[i].buttons.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
for (size_t j = 0; j < players[i].buttons.size(); ++j) {
env->SetObjectArrayElement(j_buttons, j,
Common::Android::ToJString(env, players[i].buttons[j]));
}
jobjectArray j_analogs = env->NewObjectArray(
players[i].analogs.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
for (size_t j = 0; j < players[i].analogs.size(); ++j) {
env->SetObjectArrayElement(j_analogs, j,
Common::Android::ToJString(env, players[i].analogs[j]));
}
jobjectArray j_motions = env->NewObjectArray(
players[i].motions.size(), Common::Android::GetStringClass(), env->NewStringUTF(""));
for (size_t j = 0; j < players[i].motions.size(); ++j) {
env->SetObjectArrayElement(j_motions, j,
Common::Android::ToJString(env, players[i].motions[j]));
}
auto j_vibration_enabled = static_cast<jboolean>(players[i].vibration_enabled);
auto j_vibration_strength = static_cast<jint>(players[i].vibration_strength);
auto j_body_color_left = static_cast<jlong>(players[i].body_color_left);
auto j_body_color_right = static_cast<jlong>(players[i].body_color_right);
auto j_button_color_left = static_cast<jlong>(players[i].button_color_left);
auto j_button_color_right = static_cast<jlong>(players[i].button_color_right);
auto j_profile_name = Common::Android::ToJString(env, players[i].profile_name);
auto j_use_system_vibrator = players[i].use_system_vibrator;
jobject playerInput = env->NewObject(
Common::Android::GetPlayerInputClass(), Common::Android::GetPlayerInputConstructor(),
j_connected, j_buttons, j_analogs, j_motions, j_vibration_enabled, j_vibration_strength,
j_body_color_left, j_body_color_right, j_button_color_left, j_button_color_right,
j_profile_name, j_use_system_vibrator);
env->SetObjectArrayElement(j_input_settings, i, playerInput);
}
return j_input_settings;
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInputSettings(JNIEnv* env, jobject obj,
jobjectArray j_value,
jboolean j_global) {
auto& players = Settings::values.players.GetValue(static_cast<bool>(j_global));
int playersSize = env->GetArrayLength(j_value);
for (int i = 0; i < playersSize; ++i) {
jobject jplayer = env->GetObjectArrayElement(j_value, i);
players[i].connected = static_cast<bool>(
env->GetBooleanField(jplayer, Common::Android::GetPlayerInputConnectedField()));
auto j_buttons_array = static_cast<jobjectArray>(
env->GetObjectField(jplayer, Common::Android::GetPlayerInputButtonsField()));
int buttons_size = env->GetArrayLength(j_buttons_array);
for (int j = 0; j < buttons_size; ++j) {
auto button = static_cast<jstring>(env->GetObjectArrayElement(j_buttons_array, j));
players[i].buttons[j] = Common::Android::GetJString(env, button);
}
auto j_analogs_array = static_cast<jobjectArray>(
env->GetObjectField(jplayer, Common::Android::GetPlayerInputAnalogsField()));
int analogs_size = env->GetArrayLength(j_analogs_array);
for (int j = 0; j < analogs_size; ++j) {
auto analog = static_cast<jstring>(env->GetObjectArrayElement(j_analogs_array, j));
players[i].analogs[j] = Common::Android::GetJString(env, analog);
}
auto j_motions_array = static_cast<jobjectArray>(
env->GetObjectField(jplayer, Common::Android::GetPlayerInputMotionsField()));
int motions_size = env->GetArrayLength(j_motions_array);
for (int j = 0; j < motions_size; ++j) {
auto motion = static_cast<jstring>(env->GetObjectArrayElement(j_motions_array, j));
players[i].motions[j] = Common::Android::GetJString(env, motion);
}
players[i].vibration_enabled = static_cast<bool>(
env->GetBooleanField(jplayer, Common::Android::GetPlayerInputVibrationEnabledField()));
players[i].vibration_strength = static_cast<int>(
env->GetIntField(jplayer, Common::Android::GetPlayerInputVibrationStrengthField()));
players[i].body_color_left = static_cast<u32>(
env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorLeftField()));
players[i].body_color_right = static_cast<u32>(
env->GetLongField(jplayer, Common::Android::GetPlayerInputBodyColorRightField()));
players[i].button_color_left = static_cast<u32>(
env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorLeftField()));
players[i].button_color_right = static_cast<u32>(
env->GetLongField(jplayer, Common::Android::GetPlayerInputButtonColorRightField()));
auto profileName = static_cast<jstring>(
env->GetObjectField(jplayer, Common::Android::GetPlayerInputProfileNameField()));
players[i].profile_name = Common::Android::GetJString(env, profileName);
players[i].use_system_vibrator =
env->GetBooleanField(jplayer, Common::Android::GetPlayerInputUseSystemVibratorField());
}
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveControlPlayerValues(JNIEnv* env, jobject obj) {
Settings::values.players.SetGlobal(false);
// Clear all controls from the config in case the user reverted back to globals
per_game_config->ClearControlPlayerValues();
for (size_t index = 0; index < Settings::values.players.GetValue().size(); ++index) {
per_game_config->SaveAndroidControlPlayerValues(index);
}
}
} // extern "C" } // extern "C"

View File

@ -0,0 +1,631 @@
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <common/fs/fs.h>
#include <common/fs/path_util.h>
#include <common/settings.h>
#include <hid_core/hid_types.h>
#include <jni.h>
#include "android_config.h"
#include "common/android/android_common.h"
#include "common/android/id_cache.h"
#include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h"
#include "input_common/drivers/android.h"
#include "input_common/drivers/touch_screen.h"
#include "input_common/drivers/virtual_amiibo.h"
#include "input_common/drivers/virtual_gamepad.h"
#include "native.h"
std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
bool IsHandheldOnly() {
const auto npad_style_set =
EmulationSession::GetInstance().System().HIDCore().GetSupportedStyleTag();
if (npad_style_set.fullkey == 1) {
return false;
}
if (npad_style_set.handheld == 0) {
return false;
}
return !Settings::IsDockedMode();
}
std::filesystem::path GetNameWithoutExtension(std::filesystem::path filename) {
return filename.replace_extension();
}
bool IsProfileNameValid(std::string_view profile_name) {
return profile_name.find_first_of("<>:;\"/\\|,.!?*") == std::string::npos;
}
bool ProfileExistsInFilesystem(std::string_view profile_name) {
return Common::FS::Exists(Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input" /
fmt::format("{}.ini", profile_name));
}
bool ProfileExistsInMap(const std::string& profile_name) {
return map_profiles.find(profile_name) != map_profiles.end();
}
bool SaveProfile(const std::string& profile_name, std::size_t player_index) {
if (!ProfileExistsInMap(profile_name)) {
return false;
}
Settings::values.players.GetValue()[player_index].profile_name = profile_name;
map_profiles[profile_name]->SaveAndroidControlPlayerValues(player_index);
return true;
}
bool LoadProfile(std::string& profile_name, std::size_t player_index) {
if (!ProfileExistsInMap(profile_name)) {
return false;
}
if (!ProfileExistsInFilesystem(profile_name)) {
map_profiles.erase(profile_name);
return false;
}
LOG_INFO(Config, "Loading input profile `{}`", profile_name);
Settings::values.players.GetValue()[player_index].profile_name = profile_name;
map_profiles[profile_name]->ReadAndroidControlPlayerValues(player_index);
return true;
}
void ApplyControllerConfig(size_t player_index,
const std::function<void(Core::HID::EmulatedController*)>& apply) {
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
if (player_index == 0) {
auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
handheld->EnableConfiguration();
player_one->EnableConfiguration();
apply(handheld);
apply(player_one);
handheld->DisableConfiguration();
player_one->DisableConfiguration();
handheld->SaveCurrentConfig();
player_one->SaveCurrentConfig();
} else {
auto* controller = hid_core.GetEmulatedControllerByIndex(player_index);
controller->EnableConfiguration();
apply(controller);
controller->DisableConfiguration();
controller->SaveCurrentConfig();
}
}
void ConnectController(size_t player_index, bool connected) {
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
if (player_index == 0) {
auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
handheld->EnableConfiguration();
player_one->EnableConfiguration();
if (player_one->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) {
if (connected) {
handheld->Connect();
} else {
handheld->Disconnect();
}
player_one->Disconnect();
} else {
if (connected) {
player_one->Connect();
} else {
player_one->Disconnect();
}
handheld->Disconnect();
}
handheld->DisableConfiguration();
player_one->DisableConfiguration();
handheld->SaveCurrentConfig();
player_one->SaveCurrentConfig();
} else {
auto* controller = hid_core.GetEmulatedControllerByIndex(player_index);
controller->EnableConfiguration();
if (connected) {
controller->Connect();
} else {
controller->Disconnect();
}
controller->DisableConfiguration();
controller->SaveCurrentConfig();
}
}
extern "C" {
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isHandheldOnly(JNIEnv* env,
jobject j_obj) {
return IsHandheldOnly();
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadButtonEvent(
JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_button_id, jint j_action) {
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetButtonState(
Common::Android::GetJString(env, j_guid), j_port, j_button_id, j_action != 0);
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadAxisEvent(
JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jint j_stick_id, jfloat j_value) {
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetAxisPosition(
Common::Android::GetJString(env, j_guid), j_port, j_stick_id, j_value);
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onGamePadMotionEvent(
JNIEnv* env, jobject j_obj, jstring j_guid, jint j_port, jlong j_delta_timestamp,
jfloat j_x_gyro, jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel,
jfloat j_z_accel) {
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->SetMotionState(
Common::Android::GetJString(env, j_guid), j_port, j_delta_timestamp, j_x_gyro, j_y_gyro,
j_z_gyro, j_x_accel, j_y_accel, j_z_accel);
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onReadNfcTag(JNIEnv* env, jobject j_obj,
jbyteArray j_data) {
jboolean isCopy{false};
std::span<u8> data(reinterpret_cast<u8*>(env->GetByteArrayElements(j_data, &isCopy)),
static_cast<size_t>(env->GetArrayLength(j_data)));
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->LoadAmiibo(data);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onRemoveNfcTag(JNIEnv* env, jobject j_obj) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualAmiibo()->CloseAmiibo();
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchPressed(JNIEnv* env, jobject j_obj,
jint j_id, jfloat j_x_axis,
jfloat j_y_axis) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed(
j_id, j_x_axis, j_y_axis);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchMoved(JNIEnv* env, jobject j_obj,
jint j_id, jfloat j_x_axis,
jfloat j_y_axis) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved(
j_id, j_x_axis, j_y_axis);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onTouchReleased(JNIEnv* env, jobject j_obj,
jint j_id) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(j_id);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayButtonEventImpl(
JNIEnv* env, jobject j_obj, jint j_port, jint j_button_id, jint j_action) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetButtonState(
j_port, j_button_id, j_action == 1);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onOverlayJoystickEventImpl(
JNIEnv* env, jobject j_obj, jint j_port, jint j_stick_id, jfloat j_x_axis, jfloat j_y_axis) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetStickPosition(
j_port, j_stick_id, j_x_axis, j_y_axis);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_onDeviceMotionEvent(
JNIEnv* env, jobject j_obj, jint j_port, jlong j_delta_timestamp, jfloat j_x_gyro,
jfloat j_y_gyro, jfloat j_z_gyro, jfloat j_x_accel, jfloat j_y_accel, jfloat j_z_accel) {
if (EmulationSession::GetInstance().IsRunning()) {
EmulationSession::GetInstance().GetInputSubsystem().GetVirtualGamepad()->SetMotionState(
j_port, j_delta_timestamp, j_x_gyro, j_y_gyro, j_z_gyro, j_x_accel, j_y_accel,
j_z_accel);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_reloadInputDevices(JNIEnv* env,
jobject j_obj) {
EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_registerController(JNIEnv* env,
jobject j_obj,
jobject j_device) {
EmulationSession::GetInstance().GetInputSubsystem().GetAndroid()->RegisterController(j_device);
}
jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputDevices(JNIEnv* env,
jobject j_obj) {
auto devices = EmulationSession::GetInstance().GetInputSubsystem().GetInputDevices();
jobjectArray jdevices = env->NewObjectArray(devices.size(), Common::Android::GetStringClass(),
Common::Android::ToJString(env, ""));
for (size_t i = 0; i < devices.size(); ++i) {
env->SetObjectArrayElement(jdevices, i,
Common::Android::ToJString(env, devices[i].Serialize()));
}
return jdevices;
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadInputProfiles(JNIEnv* env,
jobject j_obj) {
map_profiles.clear();
const auto input_profile_loc =
Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "input";
if (Common::FS::IsDir(input_profile_loc)) {
Common::FS::IterateDirEntries(
input_profile_loc,
[&](const std::filesystem::path& full_path) {
const auto filename = full_path.filename();
const auto name_without_ext =
Common::FS::PathToUTF8String(GetNameWithoutExtension(filename));
if (filename.extension() == ".ini" && IsProfileNameValid(name_without_ext)) {
map_profiles.insert_or_assign(
name_without_ext, std::make_unique<AndroidConfig>(
name_without_ext, Config::ConfigType::InputProfile));
}
return true;
},
Common::FS::DirEntryFilter::File);
}
}
jobjectArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getInputProfileNames(
JNIEnv* env, jobject j_obj) {
std::vector<std::string> profile_names;
profile_names.reserve(map_profiles.size());
auto it = map_profiles.cbegin();
while (it != map_profiles.cend()) {
const auto& [profile_name, config] = *it;
if (!ProfileExistsInFilesystem(profile_name)) {
it = map_profiles.erase(it);
continue;
}
profile_names.push_back(profile_name);
++it;
}
std::stable_sort(profile_names.begin(), profile_names.end());
jobjectArray j_profile_names =
env->NewObjectArray(profile_names.size(), Common::Android::GetStringClass(),
Common::Android::ToJString(env, ""));
for (size_t i = 0; i < profile_names.size(); ++i) {
env->SetObjectArrayElement(j_profile_names, i,
Common::Android::ToJString(env, profile_names[i]));
}
return j_profile_names;
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isProfileNameValid(JNIEnv* env,
jobject j_obj,
jstring j_name) {
return Common::Android::GetJString(env, j_name).find_first_of("<>:;\"/\\|,.!?*") ==
std::string::npos;
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_createProfile(JNIEnv* env,
jobject j_obj,
jstring j_name,
jint j_player_index) {
auto profile_name = Common::Android::GetJString(env, j_name);
if (ProfileExistsInMap(profile_name)) {
return false;
}
map_profiles.insert_or_assign(
profile_name,
std::make_unique<AndroidConfig>(profile_name, Config::ConfigType::InputProfile));
return SaveProfile(profile_name, j_player_index);
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_deleteProfile(JNIEnv* env,
jobject j_obj,
jstring j_name,
jint j_player_index) {
auto profile_name = Common::Android::GetJString(env, j_name);
if (!ProfileExistsInMap(profile_name)) {
return false;
}
if (!ProfileExistsInFilesystem(profile_name) ||
Common::FS::RemoveFile(map_profiles[profile_name]->GetConfigFilePath())) {
map_profiles.erase(profile_name);
}
Settings::values.players.GetValue()[j_player_index].profile_name = "";
return !ProfileExistsInMap(profile_name) && !ProfileExistsInFilesystem(profile_name);
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadProfile(JNIEnv* env, jobject j_obj,
jstring j_name,
jint j_player_index) {
auto profile_name = Common::Android::GetJString(env, j_name);
return LoadProfile(profile_name, j_player_index);
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_saveProfile(JNIEnv* env, jobject j_obj,
jstring j_name,
jint j_player_index) {
auto profile_name = Common::Android::GetJString(env, j_name);
return SaveProfile(profile_name, j_player_index);
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_loadPerGameConfiguration(
JNIEnv* env, jobject j_obj, jint j_player_index, jint j_selected_index,
jstring j_selected_profile_name) {
static constexpr size_t HANDHELD_INDEX = 8;
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
Settings::values.players.SetGlobal(false);
auto profile_name = Common::Android::GetJString(env, j_selected_profile_name);
auto* emulated_controller = hid_core.GetEmulatedControllerByIndex(j_player_index);
if (j_selected_index == 0) {
Settings::values.players.GetValue()[j_player_index].profile_name = "";
if (j_player_index == 0) {
Settings::values.players.GetValue()[HANDHELD_INDEX] = {};
}
Settings::values.players.SetGlobal(true);
emulated_controller->ReloadFromSettings();
return;
}
if (profile_name.empty()) {
return;
}
auto& player = Settings::values.players.GetValue()[j_player_index];
auto& global_player = Settings::values.players.GetValue(true)[j_player_index];
player.profile_name = profile_name;
global_player.profile_name = profile_name;
// Read from the profile into the custom player settings
LoadProfile(profile_name, j_player_index);
// Make sure the controller is connected
player.connected = true;
emulated_controller->ReloadFromSettings();
if (j_player_index > 0) {
return;
}
// Handle Handheld cases
auto& handheld_player = Settings::values.players.GetValue()[HANDHELD_INDEX];
auto* handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
if (player.controller_type == Settings::ControllerType::Handheld) {
handheld_player = player;
} else {
handheld_player = {};
}
handheld_controller->ReloadFromSettings();
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_beginMapping(JNIEnv* env, jobject j_obj,
jint jtype) {
EmulationSession::GetInstance().GetInputSubsystem().BeginMapping(
static_cast<InputCommon::Polling::InputType>(jtype));
}
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getNextInput(JNIEnv* env,
jobject j_obj) {
return Common::Android::ToJString(
env, EmulationSession::GetInstance().GetInputSubsystem().GetNextInput().Serialize());
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_stopMapping(JNIEnv* env, jobject j_obj) {
EmulationSession::GetInstance().GetInputSubsystem().StopMapping();
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_updateMappingsWithDefaultImpl(
JNIEnv* env, jobject j_obj, jint j_player_index, jstring j_device_params,
jstring j_display_name) {
auto& input_subsystem = EmulationSession::GetInstance().GetInputSubsystem();
// Clear all previous mappings
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetButtonParam(button_id, {});
});
}
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetStickParam(analog_id, {});
});
}
// Apply new mappings
auto device = Common::ParamPackage(Common::Android::GetJString(env, j_device_params));
auto button_mappings = input_subsystem.GetButtonMappingForDevice(device);
auto analog_mappings = input_subsystem.GetAnalogMappingForDevice(device);
auto display_name = Common::Android::GetJString(env, j_display_name);
for (const auto& button_mapping : button_mappings) {
const std::size_t index = button_mapping.first;
auto named_mapping = button_mapping.second;
named_mapping.Set("display", display_name);
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetButtonParam(index, named_mapping);
});
}
for (const auto& analog_mapping : analog_mappings) {
const std::size_t index = analog_mapping.first;
auto named_mapping = analog_mapping.second;
named_mapping.Set("display", display_name);
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetStickParam(index, named_mapping);
});
}
}
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonParamImpl(JNIEnv* env,
jobject j_obj,
jint j_player_index,
jint j_button) {
return Common::Android::ToJString(env, EmulationSession::GetInstance()
.System()
.HIDCore()
.GetEmulatedControllerByIndex(j_player_index)
->GetButtonParam(j_button)
.Serialize());
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setButtonParamImpl(
JNIEnv* env, jobject j_obj, jint j_player_index, jint j_button_id, jstring j_param) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetButtonParam(j_button_id,
Common::ParamPackage(Common::Android::GetJString(env, j_param)));
});
}
jstring Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStickParamImpl(JNIEnv* env,
jobject j_obj,
jint j_player_index,
jint j_stick) {
return Common::Android::ToJString(env, EmulationSession::GetInstance()
.System()
.HIDCore()
.GetEmulatedControllerByIndex(j_player_index)
->GetStickParam(j_stick)
.Serialize());
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStickParamImpl(
JNIEnv* env, jobject j_obj, jint j_player_index, jint j_stick_id, jstring j_param) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetStickParam(j_stick_id,
Common::ParamPackage(Common::Android::GetJString(env, j_param)));
});
}
jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getButtonNameImpl(JNIEnv* env,
jobject j_obj,
jstring j_param) {
return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().GetButtonName(
Common::ParamPackage(Common::Android::GetJString(env, j_param))));
}
jintArray Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getSupportedStyleTagsImpl(
JNIEnv* env, jobject j_obj, jint j_player_index) {
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
const auto npad_style_set = hid_core.GetSupportedStyleTag();
std::vector<s32> supported_indexes;
if (npad_style_set.fullkey == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Fullkey));
}
if (npad_style_set.joycon_dual == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconDual));
}
if (npad_style_set.joycon_left == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconLeft));
}
if (npad_style_set.joycon_right == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::JoyconRight));
}
if (j_player_index == 0 && npad_style_set.handheld == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::Handheld));
}
if (npad_style_set.gamecube == 1) {
supported_indexes.push_back(static_cast<u32>(Core::HID::NpadStyleIndex::GameCube));
}
jintArray j_supported_indexes = env->NewIntArray(supported_indexes.size());
env->SetIntArrayRegion(j_supported_indexes, 0, supported_indexes.size(),
supported_indexes.data());
return j_supported_indexes;
}
jint Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getStyleIndexImpl(JNIEnv* env,
jobject j_obj,
jint j_player_index) {
return static_cast<s32>(EmulationSession::GetInstance()
.System()
.HIDCore()
.GetEmulatedControllerByIndex(j_player_index)
->GetNpadStyleIndex(true));
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_setStyleIndexImpl(JNIEnv* env,
jobject j_obj,
jint j_player_index,
jint j_style_index) {
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
auto type = static_cast<Core::HID::NpadStyleIndex>(j_style_index);
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetNpadStyleIndex(type);
});
if (j_player_index == 0) {
auto* handheld = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld);
auto* player_one = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1);
ConnectController(j_player_index,
player_one->IsConnected(true) || handheld->IsConnected(true));
}
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_isControllerImpl(JNIEnv* env,
jobject j_obj,
jstring jparams) {
return static_cast<jint>(EmulationSession::GetInstance().GetInputSubsystem().IsController(
Common::ParamPackage(Common::Android::GetJString(env, jparams))));
}
jboolean Java_org_yuzu_yuzu_1emu_features_input_NativeInput_getIsConnected(JNIEnv* env,
jobject j_obj,
jint j_player_index) {
auto& hid_core = EmulationSession::GetInstance().System().HIDCore();
auto* controller = hid_core.GetEmulatedControllerByIndex(static_cast<size_t>(j_player_index));
if (j_player_index == 0 &&
controller->GetNpadStyleIndex(true) == Core::HID::NpadStyleIndex::Handheld) {
return hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld)->IsConnected(true);
}
return controller->IsConnected(true);
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_connectControllersImpl(
JNIEnv* env, jobject j_obj, jbooleanArray j_connected) {
jboolean isCopy = false;
auto j_connected_array_size = env->GetArrayLength(j_connected);
jboolean* j_connected_array = env->GetBooleanArrayElements(j_connected, &isCopy);
for (int i = 0; i < j_connected_array_size; ++i) {
ConnectController(i, j_connected_array[i]);
}
}
void Java_org_yuzu_yuzu_1emu_features_input_NativeInput_resetControllerMappings(
JNIEnv* env, jobject j_obj, jint j_player_index) {
// Clear all previous mappings
for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; ++button_id) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetButtonParam(button_id, {});
});
}
for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; ++analog_id) {
ApplyControllerConfig(j_player_index, [&](Core::HID::EmulatedController* controller) {
controller->SetStickParam(analog_id, {});
});
}
}
} // extern "C"

View File

@ -0,0 +1,142 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="1000dp"
android:height="1000dp"
android:viewportWidth="1000"
android:viewportHeight="1000">
<group android:name="_R_G">
<group
android:name="_R_G_L_0_G"
android:pivotX="100"
android:pivotY="100"
android:scaleX="4.5"
android:scaleY="4.5"
android:translateX="400"
android:translateY="400">
<path
android:name="_R_G_L_0_G_D_0_P_0"
android:fillAlpha="1"
android:fillColor="?attr/colorSecondaryContainer"
android:fillType="nonZero"
android:pathData=" M198.56 100 C198.56,154.43 154.43,198.56 100,198.56 C45.57,198.56 1.44,154.43 1.44,100 C1.44,45.57 45.57,1.44 100,1.44 C154.43,1.44 198.56,45.57 198.56,100c " />
<path
android:name="_R_G_L_0_G_D_2_P_0"
android:fillAlpha="0.8"
android:fillColor="?attr/colorOnSecondaryContainer"
android:fillType="nonZero"
android:pathData=" M50.14 151.21 C50.53,150.18 89.6,49.87 90.1,48.63 C90.1,48.63 90.67,47.2 90.67,47.2 C90.67,47.2 101.67,47.2 101.67,47.2 C101.67,47.2 112.67,47.2 112.67,47.2 C112.67,47.2 133.47,99.12 133.47,99.12 C144.91,127.68 154.32,151.17 154.38,151.33 C154.47,151.56 152.2,151.6 143.14,151.55 C143.14,151.55 131.79,151.48 131.79,151.48 C131.79,151.48 127.22,139.57 127.22,139.57 C127.22,139.57 122.65,127.66 122.65,127.66 C122.65,127.66 101.68,127.73 101.68,127.73 C101.68,127.73 80.71,127.8 80.71,127.8 C80.71,127.8 76.38,139.71 76.38,139.71 C76.38,139.71 72.06,151.62 72.06,151.62 C72.06,151.62 61.02,151.62 61.02,151.62 C50.61,151.62 50,151.55 50.14,151.22 C50.14,151.22 50.14,151.21 50.14,151.21c M115.86 110.06 C115.8,109.91 112.55,101.13 108.62,90.56 C104.7,80 101.42,71.43 101.34,71.53 C101.22,71.66 92.84,94.61 87.25,110.06 C87.17,110.29 90.13,110.34 101.56,110.34 C113,110.34 115.95,110.28 115.86,110.06c " />
</group>
</group>
<group android:name="time_group" />
</vector>
</aapt:attr>
<target android:name="_R_G_L_0_G">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="100"
android:propertyName="scaleX"
android:startOffset="0"
android:valueFrom="4.5"
android:valueTo="3.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="100"
android:propertyName="scaleY"
android:startOffset="0"
android:valueFrom="4.5"
android:valueTo="3.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="234"
android:propertyName="scaleX"
android:startOffset="100"
android:valueFrom="3.75"
android:valueTo="3.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="234"
android:propertyName="scaleY"
android:startOffset="100"
android:valueFrom="3.75"
android:valueTo="3.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="167"
android:propertyName="scaleX"
android:startOffset="334"
android:valueFrom="3.75"
android:valueTo="4.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="167"
android:propertyName="scaleY"
android:startOffset="334"
android:valueFrom="3.75"
android:valueTo="4.75"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="67"
android:propertyName="scaleX"
android:startOffset="501"
android:valueFrom="4.75"
android:valueTo="4.5"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="67"
android:propertyName="scaleY"
android:startOffset="501"
android:valueFrom="4.75"
android:valueTo="4.5"
android:valueType="floatType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</set>
</aapt:attr>
</target>
<target android:name="time_group">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="1034"
android:propertyName="translateX"
android:startOffset="0"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M700,480q-25,0 -42.5,-17.5T640,420q0,-25 17.5,-42.5T700,360q25,0 42.5,17.5T760,420q0,25 -17.5,42.5T700,480ZM366,480ZM280,600v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM160,720q-33,0 -56.5,-23.5T80,640v-320q0,-34 24,-57.5t58,-23.5h77l81,81L160,320v320h366L55,169l57,-57 736,736 -57,57 -185,-185L160,720ZM880,640q0,26 -14,46t-37,29l-29,-29v-366L434,320l-80,-80h446q33,0 56.5,23.5T880,320v320ZM617,503Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M21,12l-4.37,6.16C16.26,18.68 15.65,19 15,19h-3l0,-6H9v-3H3V7c0,-1.1 0.9,-2 2,-2h10c0.65,0 1.26,0.31 1.63,0.84L21,12zM10,15H7v-3H5v3H2v2h3v3h2v-3h3V15z" />
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M21,5H3C1.9,5 1,5.9 1,7v10c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2V7C23,5.9 22.1,5 21,5zM18,17H6V7h12V17z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M15,11.25h1.5v1.5h-1.5z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12.5,11.25h1.5v1.5h-1.5z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M10,11.25h1.5v1.5h-1.5z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M7.5,11.25h1.5v1.5h-1.5z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
</vector>

View File

@ -0,0 +1,118 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="1000dp"
android:height="1000dp"
android:viewportWidth="1000"
android:viewportHeight="1000">
<group android:name="_R_G">
<group
android:name="_R_G_L_1_G"
android:pivotX="100"
android:pivotY="100"
android:scaleX="5"
android:scaleY="5"
android:translateX="400"
android:translateY="400">
<path
android:name="_R_G_L_1_G_D_0_P_0"
android:pathData=" M100 199.39 C59.8,199.39 23.56,175.17 8.18,138.04 C-7.2,100.9 1.3,58.15 29.73,29.72 C58.15,1.3 100.9,-7.21 138.04,8.18 C175.18,23.56 199.39,59.8 199.39,100 C199.33,154.87 154.87,199.33 100,199.39c "
android:strokeWidth="1"
android:strokeAlpha="0.6"
android:strokeColor="?attr/colorOutline"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
<group
android:name="_R_G_L_0_G_T_1"
android:scaleX="5"
android:scaleY="5"
android:translateX="500"
android:translateY="500">
<group
android:name="_R_G_L_0_G"
android:translateX="-100"
android:translateY="-100">
<path
android:name="_R_G_L_0_G_D_0_P_0"
android:fillAlpha="1"
android:fillColor="?attr/colorSecondaryContainer"
android:fillType="nonZero"
android:pathData=" M100.45 28.02 C140.63,28.02 173.2,60.59 173.2,100.77 C173.2,140.95 140.63,173.52 100.45,173.52 C60.27,173.52 27.7,140.95 27.7,100.77 C27.7,60.59 60.27,28.02 100.45,28.02c " />
<path
android:name="_R_G_L_0_G_D_2_P_0"
android:fillAlpha="0.8"
android:fillColor="?attr/colorOnSecondaryContainer"
android:fillType="nonZero"
android:pathData=" M100.45 50.26 C128.62,50.26 151.46,73.1 151.46,101.28 C151.46,129.45 128.62,152.29 100.45,152.29 C72.27,152.29 49.43,129.45 49.43,101.28 C49.43,73.1 72.27,50.26 100.45,50.26c " />
</group>
</group>
</group>
<group android:name="time_group" />
</vector>
</aapt:attr>
<target android:name="_R_G_L_0_G_T_1">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="267"
android:pathData="M 500,500C 500,500 364,500 364,500"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="0">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="234"
android:pathData="M 364,500C 364,500 364,500 364,500"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="267">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="133"
android:pathData="M 364,500C 364,500 525,500 525,500"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="501">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="100"
android:pathData="M 525,500C 525,500 500,500 500,500"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="634">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</set>
</aapt:attr>
</target>
<target android:name="time_group">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="968"
android:propertyName="translateX"
android:startOffset="0"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

View File

@ -0,0 +1,173 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="1000dp"
android:height="1000dp"
android:viewportWidth="1000"
android:viewportHeight="1000">
<group android:name="_R_G">
<group
android:name="_R_G_L_1_G"
android:pivotX="100"
android:pivotY="100"
android:scaleX="5"
android:scaleY="5"
android:translateX="400"
android:translateY="400">
<path
android:name="_R_G_L_1_G_D_0_P_0"
android:pathData=" M100 199.39 C59.8,199.39 23.56,175.17 8.18,138.04 C-7.2,100.9 1.3,58.15 29.73,29.72 C58.15,1.3 100.9,-7.21 138.04,8.18 C175.18,23.56 199.39,59.8 199.39,100 C199.33,154.87 154.87,199.33 100,199.39c "
android:strokeWidth="1"
android:strokeAlpha="0.6"
android:strokeColor="?attr/colorOutline"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
<group
android:name="_R_G_L_0_G_T_1"
android:scaleX="5"
android:scaleY="5"
android:translateX="500"
android:translateY="500">
<group
android:name="_R_G_L_0_G"
android:translateX="-100"
android:translateY="-100">
<path
android:name="_R_G_L_0_G_D_0_P_0"
android:fillAlpha="1"
android:fillColor="?attr/colorSecondaryContainer"
android:fillType="nonZero"
android:pathData=" M100.45 28.02 C140.63,28.02 173.2,60.59 173.2,100.77 C173.2,140.95 140.63,173.52 100.45,173.52 C60.27,173.52 27.7,140.95 27.7,100.77 C27.7,60.59 60.27,28.02 100.45,28.02c " />
<path
android:name="_R_G_L_0_G_D_2_P_0"
android:fillAlpha="0.8"
android:fillColor="?attr/colorOnSecondaryContainer"
android:fillType="nonZero"
android:pathData=" M100.45 50.26 C128.62,50.26 151.46,73.1 151.46,101.28 C151.46,129.45 128.62,152.29 100.45,152.29 C72.27,152.29 49.43,129.45 49.43,101.28 C49.43,73.1 72.27,50.26 100.45,50.26c " />
</group>
</group>
</group>
<group android:name="time_group" />
</vector>
</aapt:attr>
<target android:name="_R_G_L_0_G_T_1">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="267"
android:pathData="M 500,500C 500,500 364,500 364,500"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="0">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="234"
android:pathData="M 364,500C 364,500 364,500 364,500"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="267">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="133"
android:pathData="M 364,500C 364,500 525,500 525,500"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="501">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="100"
android:pathData="M 525,500C 525,500 500,500 500,500"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="634">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="400"
android:pathData="M 500,500C 500,500 500,500 500,500"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="734">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="267"
android:pathData="M 500,500C 500,500 500,364 500,364"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="1134">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="234"
android:pathData="M 500,364C 500,364 500,364 500,364"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="1401">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0.333 0.667,0.667 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="133"
android:pathData="M 500,364C 500,364 500,535 500,535"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="1635">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="100"
android:pathData="M 500,535C 500,535 500,500 500,500"
android:propertyName="translateXY"
android:propertyXName="translateX"
android:propertyYName="translateY"
android:startOffset="1768">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.333,0 0.667,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</set>
</aapt:attr>
</target>
<target android:name="time_group">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="2269"
android:propertyName="translateX"
android:startOffset="0"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/setting_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="72dp"
android:padding="16dp"
android:nextFocusLeft="@id/button_options">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
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="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textSize="17sp"
app:lineHeight="22dp"
tools:text="Setting Name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_setting_value"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:textAlignment="viewStart"
android:textStyle="bold"
android:textSize="13sp"
tools:text="1x" />
</LinearLayout>
<Button
android:id="@+id/button_options"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:nextFocusRight="@id/setting_body"
app:icon="@drawable/ic_more_vert"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/list_profiles"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fadeScrollbars="false" />

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:focusedByDefault="true"
android:orientation="horizontal"
android:gravity="center">
<ImageView
android:id="@+id/image_stick_animation"
android:layout_width="@dimen/mapping_anim_size"
android:layout_height="@dimen/mapping_anim_size"
tools:src="@drawable/stick_two_direction_anim" />
<ImageView
android:id="@+id/image_button_animation"
android:layout_width="@dimen/mapping_anim_size"
android:layout_height="@dimen/mapping_anim_size"
android:layout_marginStart="48dp"
tools:src="@drawable/button_anim" />
</LinearLayout>

View File

@ -0,0 +1,74 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:paddingHorizontal="20dp"
android:paddingVertical="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
style="@style/TextAppearance.Material3.HeadlineMedium"
android:layout_width="0dp"
android:layout_height="0dp"
android:textAlignment="viewStart"
android:gravity="start|center_vertical"
android:textSize="17sp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="@+id/button_layout"
app:layout_constraintEnd_toStartOf="@+id/button_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:lineHeight="28dp"
tools:text="My profile" />
<LinearLayout
android:id="@+id/button_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/button_new"
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/create_new_profile"
android:tooltipText="@string/create_new_profile"
app:icon="@drawable/ic_new_label" />
<Button
android:id="@+id/button_delete"
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/delete"
android:tooltipText="@string/delete"
app:icon="@drawable/ic_delete" />
<Button
android:id="@+id/button_save"
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/save"
android:tooltipText="@string/save"
app:icon="@drawable/ic_save" />
<Button
android:id="@+id/button_load"
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/load"
android:tooltipText="@string/load"
app:icon="@drawable/ic_import" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/setting_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="72dp"
android:padding="16dp"
android:nextFocusRight="@id/button_options">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
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="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textSize="17sp"
app:lineHeight="22dp"
tools:text="Setting Name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_setting_value"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_small"
android:textAlignment="viewStart"
android:textStyle="bold"
android:textSize="13sp"
tools:text="1x" />
</LinearLayout>
<Button
android:id="@+id/button_options"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:nextFocusLeft="@id/setting_body"
app:icon="@drawable/ic_more_vert"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface" />
</LinearLayout>
</RelativeLayout>

View File

@ -17,8 +17,13 @@
android:title="@string/per_game_settings" /> android:title="@string/per_game_settings" />
<item <item
android:id="@+id/menu_overlay_controls" android:id="@+id/menu_controls"
android:icon="@drawable/ic_controller" android:icon="@drawable/ic_controller"
android:title="@string/preferences_controls" />
<item
android:id="@+id/menu_overlay_controls"
android:icon="@drawable/ic_overlay"
android:title="@string/emulation_input_overlay" /> android:title="@string/emulation_input_overlay" />
<item <item

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/invert_axis"
android:title="@string/invert_axis"
android:visible="false" />
<item
android:id="@+id/invert_button"
android:title="@string/invert_button"
android:visible="false" />
<item
android:id="@+id/toggle_button"
android:title="@string/toggle_button"
android:visible="false" />
<item
android:id="@+id/turbo_button"
android:title="@string/turbo_button"
android:visible="false" />
<item
android:id="@+id/set_threshold"
android:title="@string/set_threshold"
android:visible="false" />
<item
android:id="@+id/toggle_axis"
android:title="@string/toggle_axis"
android:visible="false" />
</menu>

View File

@ -26,7 +26,7 @@
<fragment <fragment
android:id="@+id/settingsSearchFragment" android:id="@+id/settingsSearchFragment"
android:name="org.yuzu.yuzu_emu.fragments.SettingsSearchFragment" android:name="org.yuzu.yuzu_emu.features.settings.ui.SettingsSearchFragment"
android:label="SettingsSearchFragment" /> android:label="SettingsSearchFragment" />
</navigation> </navigation>

View File

@ -2,4 +2,6 @@
<resources> <resources>
<dimen name="spacing_navigation">0dp</dimen> <dimen name="spacing_navigation">0dp</dimen>
<dimen name="spacing_navigation_rail">80dp</dimen> <dimen name="spacing_navigation_rail">80dp</dimen>
<dimen name="mapping_anim_size">100dp</dimen>
</resources> </resources>

View File

@ -18,4 +18,6 @@
<dimen name="dialog_margin">20dp</dimen> <dimen name="dialog_margin">20dp</dimen>
<dimen name="elevated_app_bar">3dp</dimen> <dimen name="elevated_app_bar">3dp</dimen>
<dimen name="mapping_anim_size">75dp</dimen>
</resources> </resources>

View File

@ -255,6 +255,92 @@
<string name="audio_volume">Volume</string> <string name="audio_volume">Volume</string>
<string name="audio_volume_description">Specifies the volume of audio output.</string> <string name="audio_volume_description">Specifies the volume of audio output.</string>
<!-- Input strings -->
<string name="buttons">Buttons</string>
<string name="button_a">A</string>
<string name="button_b">B</string>
<string name="button_x">X</string>
<string name="button_y">Y</string>
<string name="button_plus">Plus</string>
<string name="button_minus">Minus</string>
<string name="button_home">Home</string>
<string name="button_capture">Capture</string>
<string name="start_pause">Start/Pause</string>
<string name="dpad">D-Pad</string>
<string name="up">Up</string>
<string name="down">Down</string>
<string name="left">Left</string>
<string name="right">Right</string>
<string name="left_stick">Left stick</string>
<string name="control_stick">Control stick</string>
<string name="right_stick">Right stick</string>
<string name="c_stick">C-Stick</string>
<string name="pressed">Pressed</string>
<string name="range">Range</string>
<string name="deadzone">Deadzone</string>
<string name="modifier">Modifier</string>
<string name="modifier_range">Modifier range</string>
<string name="triggers">Triggers</string>
<string name="button_l">L</string>
<string name="button_r">R</string>
<string name="button_zl">ZL</string>
<string name="button_zr">ZR</string>
<string name="button_sl_left">Left SL</string>
<string name="button_sr_left">Left SR</string>
<string name="button_sl_right">Right SL</string>
<string name="button_sr_right">Right SR</string>
<string name="button_z">Z</string>
<string name="invalid">Invalid</string>
<string name="not_set">Not set</string>
<string name="unknown">Unknown</string>
<string name="qualified_hat">%1$s%2$s%3$sHat %4$s</string>
<string name="qualified_button_stick_axis">%1$s%2$s%3$sAxis %4$s</string>
<string name="qualified_button">%1$s%2$s%3$sButton %4$s</string>
<string name="qualified_axis">Axis %1$s%2$s</string>
<string name="unused">Unused</string>
<string name="input_prompt">Move or press an input</string>
<string name="unsupported_input">Unsupported input type</string>
<string name="input_mapping_filter">Input mapping filter</string>
<string name="input_mapping_filter_description">Select a device to filter mapping inputs</string>
<string name="auto_map">Auto-map a controller</string>
<string name="auto_map_description">Select a device to attempt auto-mapping</string>
<string name="attempted_auto_map">Attempted auto-map with %1$s</string>
<string name="controller_type">Controller type</string>
<string name="pro_controller">Pro Controller</string>
<string name="handheld">Handheld</string>
<string name="dual_joycons">Dual Joycons</string>
<string name="left_joycon">Left Joycon</string>
<string name="right_joycon">Right Joycon</string>
<string name="gamecube_controller">GameCube Controller</string>
<string name="invert_axis">Invert axis</string>
<string name="invert_button">Invert button</string>
<string name="toggle_button">Toggle button</string>
<string name="turbo_button">Turbo button</string>
<string name="set_threshold">Set threshold</string>
<string name="toggle_axis">Toggle axis</string>
<string name="connected">Connected</string>
<string name="use_system_vibrator">Use system vibrator</string>
<string name="input_overlay">Input overlay</string>
<string name="vibration">Vibration</string>
<string name="vibration_strength">Vibration strength</string>
<string name="profile">Profile</string>
<string name="create_new_profile">Create new profile</string>
<string name="enter_profile_name">Enter profile name</string>
<string name="profile_name_already_exists">Profile name already exists</string>
<string name="invalid_profile_name">Invalid profile name</string>
<string name="use_global_input_configuration">Use global input configuration</string>
<string name="player_num_profile">Player %d profile</string>
<string name="delete_input_profile">Delete input profile</string>
<string name="delete_input_profile_description">Are you sure that you want to delete this profile? This is not recoverable.</string>
<string name="stick_map_description">Move a stick left and then up or press a button</string>
<string name="button_map_description">Press a button or move a trigger/stick</string>
<string name="map_dpad_direction">Map to D-Pad %1$s</string>
<string name="map_control">Map to %1$s</string>
<string name="failed_to_load_profile">Failed to load profile</string>
<string name="failed_to_save_profile">Failed to save profile</string>
<string name="reset_mapping">Reset mappings</string>
<string name="reset_mapping_description">Are you sure that you want to reset all mappings for this controller to default? This cannot be undone.</string>
<!-- Miscellaneous --> <!-- Miscellaneous -->
<string name="slider_default">Default</string> <string name="slider_default">Default</string>
<string name="ini_saved">Saved settings</string> <string name="ini_saved">Saved settings</string>
@ -292,6 +378,10 @@
<string name="more_options">More options</string> <string name="more_options">More options</string>
<string name="use_global_setting">Use global setting</string> <string name="use_global_setting">Use global setting</string>
<string name="operation_completed_successfully">The operation completed successfully</string> <string name="operation_completed_successfully">The operation completed successfully</string>
<string name="retry">Retry</string>
<string name="confirm">Confirm</string>
<string name="load">Load</string>
<string name="save">Save</string>
<!-- GPU driver installation --> <!-- GPU driver installation -->
<string name="select_gpu_driver">Select GPU driver</string> <string name="select_gpu_driver">Select GPU driver</string>
@ -313,6 +403,9 @@
<string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string> <string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string>
<string name="preferences_audio">Audio</string> <string name="preferences_audio">Audio</string>
<string name="preferences_audio_description">Output engine, volume</string> <string name="preferences_audio_description">Output engine, volume</string>
<string name="preferences_controls">Controls</string>
<string name="preferences_controls_description">Map controller input</string>
<string name="preferences_player">Player %d</string>
<string name="preferences_theme">Theme and color</string> <string name="preferences_theme">Theme and color</string>
<string name="preferences_debug">Debug</string> <string name="preferences_debug">Debug</string>
<string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string>

View File

@ -65,6 +65,30 @@ static jclass s_boolean_class;
static jmethodID s_boolean_constructor; static jmethodID s_boolean_constructor;
static jfieldID s_boolean_value_field; static jfieldID s_boolean_value_field;
static jclass s_player_input_class;
static jmethodID s_player_input_constructor;
static jfieldID s_player_input_connected_field;
static jfieldID s_player_input_buttons_field;
static jfieldID s_player_input_analogs_field;
static jfieldID s_player_input_motions_field;
static jfieldID s_player_input_vibration_enabled_field;
static jfieldID s_player_input_vibration_strength_field;
static jfieldID s_player_input_body_color_left_field;
static jfieldID s_player_input_body_color_right_field;
static jfieldID s_player_input_button_color_left_field;
static jfieldID s_player_input_button_color_right_field;
static jfieldID s_player_input_profile_name_field;
static jfieldID s_player_input_use_system_vibrator_field;
static jclass s_yuzu_input_device_interface;
static jmethodID s_yuzu_input_device_get_name;
static jmethodID s_yuzu_input_device_get_guid;
static jmethodID s_yuzu_input_device_get_port;
static jmethodID s_yuzu_input_device_get_supports_vibration;
static jmethodID s_yuzu_input_device_vibrate;
static jmethodID s_yuzu_input_device_get_axes;
static jmethodID s_yuzu_input_device_has_keys;
static constexpr jint JNI_VERSION = JNI_VERSION_1_6; static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
namespace Common::Android { namespace Common::Android {
@ -276,6 +300,94 @@ jfieldID GetBooleanValueField() {
return s_boolean_value_field; return s_boolean_value_field;
} }
jclass GetPlayerInputClass() {
return s_player_input_class;
}
jmethodID GetPlayerInputConstructor() {
return s_player_input_constructor;
}
jfieldID GetPlayerInputConnectedField() {
return s_player_input_connected_field;
}
jfieldID GetPlayerInputButtonsField() {
return s_player_input_buttons_field;
}
jfieldID GetPlayerInputAnalogsField() {
return s_player_input_analogs_field;
}
jfieldID GetPlayerInputMotionsField() {
return s_player_input_motions_field;
}
jfieldID GetPlayerInputVibrationEnabledField() {
return s_player_input_vibration_enabled_field;
}
jfieldID GetPlayerInputVibrationStrengthField() {
return s_player_input_vibration_strength_field;
}
jfieldID GetPlayerInputBodyColorLeftField() {
return s_player_input_body_color_left_field;
}
jfieldID GetPlayerInputBodyColorRightField() {
return s_player_input_body_color_right_field;
}
jfieldID GetPlayerInputButtonColorLeftField() {
return s_player_input_button_color_left_field;
}
jfieldID GetPlayerInputButtonColorRightField() {
return s_player_input_button_color_right_field;
}
jfieldID GetPlayerInputProfileNameField() {
return s_player_input_profile_name_field;
}
jfieldID GetPlayerInputUseSystemVibratorField() {
return s_player_input_use_system_vibrator_field;
}
jclass GetYuzuInputDeviceInterface() {
return s_yuzu_input_device_interface;
}
jmethodID GetYuzuDeviceGetName() {
return s_yuzu_input_device_get_name;
}
jmethodID GetYuzuDeviceGetGUID() {
return s_yuzu_input_device_get_guid;
}
jmethodID GetYuzuDeviceGetPort() {
return s_yuzu_input_device_get_port;
}
jmethodID GetYuzuDeviceGetSupportsVibration() {
return s_yuzu_input_device_get_supports_vibration;
}
jmethodID GetYuzuDeviceVibrate() {
return s_yuzu_input_device_vibrate;
}
jmethodID GetYuzuDeviceGetAxes() {
return s_yuzu_input_device_get_axes;
}
jmethodID GetYuzuDeviceHasKeys() {
return s_yuzu_input_device_has_keys;
}
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
@ -387,6 +499,55 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z"); s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z");
env->DeleteLocalRef(boolean_class); env->DeleteLocalRef(boolean_class);
const jclass player_input_class =
env->FindClass("org/yuzu/yuzu_emu/features/input/model/PlayerInput");
s_player_input_class = reinterpret_cast<jclass>(env->NewGlobalRef(player_input_class));
s_player_input_constructor = env->GetMethodID(
player_input_class, "<init>",
"(Z[Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;ZIJJJJLjava/lang/String;Z)V");
s_player_input_connected_field = env->GetFieldID(player_input_class, "connected", "Z");
s_player_input_buttons_field =
env->GetFieldID(player_input_class, "buttons", "[Ljava/lang/String;");
s_player_input_analogs_field =
env->GetFieldID(player_input_class, "analogs", "[Ljava/lang/String;");
s_player_input_motions_field =
env->GetFieldID(player_input_class, "motions", "[Ljava/lang/String;");
s_player_input_vibration_enabled_field =
env->GetFieldID(player_input_class, "vibrationEnabled", "Z");
s_player_input_vibration_strength_field =
env->GetFieldID(player_input_class, "vibrationStrength", "I");
s_player_input_body_color_left_field =
env->GetFieldID(player_input_class, "bodyColorLeft", "J");
s_player_input_body_color_right_field =
env->GetFieldID(player_input_class, "bodyColorRight", "J");
s_player_input_button_color_left_field =
env->GetFieldID(player_input_class, "buttonColorLeft", "J");
s_player_input_button_color_right_field =
env->GetFieldID(player_input_class, "buttonColorRight", "J");
s_player_input_profile_name_field =
env->GetFieldID(player_input_class, "profileName", "Ljava/lang/String;");
s_player_input_use_system_vibrator_field =
env->GetFieldID(player_input_class, "useSystemVibrator", "Z");
env->DeleteLocalRef(player_input_class);
const jclass yuzu_input_device_interface =
env->FindClass("org/yuzu/yuzu_emu/features/input/YuzuInputDevice");
s_yuzu_input_device_interface =
reinterpret_cast<jclass>(env->NewGlobalRef(yuzu_input_device_interface));
s_yuzu_input_device_get_name =
env->GetMethodID(yuzu_input_device_interface, "getName", "()Ljava/lang/String;");
s_yuzu_input_device_get_guid =
env->GetMethodID(yuzu_input_device_interface, "getGUID", "()Ljava/lang/String;");
s_yuzu_input_device_get_port = env->GetMethodID(yuzu_input_device_interface, "getPort", "()I");
s_yuzu_input_device_get_supports_vibration =
env->GetMethodID(yuzu_input_device_interface, "getSupportsVibration", "()Z");
s_yuzu_input_device_vibrate = env->GetMethodID(yuzu_input_device_interface, "vibrate", "(F)V");
s_yuzu_input_device_get_axes =
env->GetMethodID(yuzu_input_device_interface, "getAxes", "()[Ljava/lang/Integer;");
s_yuzu_input_device_has_keys =
env->GetMethodID(yuzu_input_device_interface, "hasKeys", "([I)[Z");
env->DeleteLocalRef(yuzu_input_device_interface);
// Initialize Android Storage // Initialize Android Storage
Common::FS::Android::RegisterCallbacks(env, s_native_library_class); Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
@ -416,6 +577,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
env->DeleteGlobalRef(s_double_class); env->DeleteGlobalRef(s_double_class);
env->DeleteGlobalRef(s_integer_class); env->DeleteGlobalRef(s_integer_class);
env->DeleteGlobalRef(s_boolean_class); env->DeleteGlobalRef(s_boolean_class);
env->DeleteGlobalRef(s_player_input_class);
env->DeleteGlobalRef(s_yuzu_input_device_interface);
// UnInitialize applets // UnInitialize applets
SoftwareKeyboard::CleanupJNI(env); SoftwareKeyboard::CleanupJNI(env);

View File

@ -85,4 +85,28 @@ jclass GetBooleanClass();
jmethodID GetBooleanConstructor(); jmethodID GetBooleanConstructor();
jfieldID GetBooleanValueField(); jfieldID GetBooleanValueField();
jclass GetPlayerInputClass();
jmethodID GetPlayerInputConstructor();
jfieldID GetPlayerInputConnectedField();
jfieldID GetPlayerInputButtonsField();
jfieldID GetPlayerInputAnalogsField();
jfieldID GetPlayerInputMotionsField();
jfieldID GetPlayerInputVibrationEnabledField();
jfieldID GetPlayerInputVibrationStrengthField();
jfieldID GetPlayerInputBodyColorLeftField();
jfieldID GetPlayerInputBodyColorRightField();
jfieldID GetPlayerInputButtonColorLeftField();
jfieldID GetPlayerInputButtonColorRightField();
jfieldID GetPlayerInputProfileNameField();
jfieldID GetPlayerInputUseSystemVibratorField();
jclass GetYuzuInputDeviceInterface();
jmethodID GetYuzuDeviceGetName();
jmethodID GetYuzuDeviceGetGUID();
jmethodID GetYuzuDeviceGetPort();
jmethodID GetYuzuDeviceGetSupportsVibration();
jmethodID GetYuzuDeviceVibrate();
jmethodID GetYuzuDeviceGetAxes();
jmethodID GetYuzuDeviceHasKeys();
} // namespace Common::Android } // namespace Common::Android

View File

@ -395,6 +395,10 @@ struct PlayerInput {
u32 button_color_left; u32 button_color_left;
u32 button_color_right; u32 button_color_right;
std::string profile_name; std::string profile_name;
// This is meant to tell the Android frontend whether to use a device's built-in vibration
// motor or a controller's vibrations.
bool use_system_vibrator;
}; };
struct TouchscreenInput { struct TouchscreenInput {

View File

@ -138,6 +138,7 @@ void Config::ReadPlayerValues(const std::size_t player_index) {
if (profile_name.empty()) { if (profile_name.empty()) {
// Use the global input config // Use the global input config
player = Settings::values.players.GetValue(true)[player_index]; player = Settings::values.players.GetValue(true)[player_index];
player.profile_name = "";
return; return;
} }
player.profile_name = profile_name; player.profile_name = profile_name;

View File

@ -176,16 +176,19 @@ void EmulatedController::LoadDevices() {
camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"}; camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"};
ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"}; ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"};
nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"}; nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"};
android_params = Common::ParamPackage{"engine:android,port:100"};
} }
output_params[LeftIndex] = left_joycon; output_params[LeftIndex] = left_joycon;
output_params[RightIndex] = right_joycon; output_params[RightIndex] = right_joycon;
output_params[2] = camera_params[1]; output_params[2] = camera_params[1];
output_params[3] = nfc_params[0]; output_params[3] = nfc_params[0];
output_params[4] = android_params;
output_params[LeftIndex].Set("output", true); output_params[LeftIndex].Set("output", true);
output_params[RightIndex].Set("output", true); output_params[RightIndex].Set("output", true);
output_params[2].Set("output", true); output_params[2].Set("output", true);
output_params[3].Set("output", true); output_params[3].Set("output", true);
output_params[4].Set("output", true);
LoadTASParams(); LoadTASParams();
LoadVirtualGamepadParams(); LoadVirtualGamepadParams();
@ -578,6 +581,9 @@ void EmulatedController::DisableConfiguration() {
// Get Joycon colors before turning on the controller // Get Joycon colors before turning on the controller
for (const auto& color_device : color_devices) { for (const auto& color_device : color_devices) {
if (color_device == nullptr) {
continue;
}
color_device->ForceUpdate(); color_device->ForceUpdate();
} }
@ -1277,6 +1283,10 @@ bool EmulatedController::SetVibration(DeviceIndex device_index, const VibrationV
.high_frequency = vibration.high_frequency, .high_frequency = vibration.high_frequency,
.type = type, .type = type,
}; };
// Send vibrations to Android's input overlay
output_devices[4]->SetVibration(status);
return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success; return output_devices[index]->SetVibration(status) == Common::Input::DriverResult::Success;
} }

View File

@ -21,7 +21,7 @@
namespace Core::HID { namespace Core::HID {
const std::size_t max_emulated_controllers = 2; const std::size_t max_emulated_controllers = 2;
const std::size_t output_devices_size = 4; const std::size_t output_devices_size = 5;
struct ControllerMotionInfo { struct ControllerMotionInfo {
Common::Input::MotionStatus raw_status{}; Common::Input::MotionStatus raw_status{};
MotionInput emulated{}; MotionInput emulated{};
@ -597,6 +597,7 @@ private:
CameraParams camera_params; CameraParams camera_params;
RingAnalogParams ring_params; RingAnalogParams ring_params;
NfcParams nfc_params; NfcParams nfc_params;
Common::ParamPackage android_params;
OutputParams output_params; OutputParams output_params;
ButtonDevices button_devices; ButtonDevices button_devices;

View File

@ -2,8 +2,6 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
add_library(input_common STATIC add_library(input_common STATIC
drivers/android.cpp
drivers/android.h
drivers/camera.cpp drivers/camera.cpp
drivers/camera.h drivers/camera.h
drivers/keyboard.cpp drivers/keyboard.cpp
@ -94,3 +92,11 @@ target_link_libraries(input_common PUBLIC hid_core PRIVATE common Boost::headers
if (YUZU_USE_PRECOMPILED_HEADERS) if (YUZU_USE_PRECOMPILED_HEADERS)
target_precompile_headers(input_common PRIVATE precompiled_headers.h) target_precompile_headers(input_common PRIVATE precompiled_headers.h)
endif() endif()
if (ANDROID)
target_sources(input_common PRIVATE
drivers/android.cpp
drivers/android.h
)
target_link_libraries(input_common PRIVATE android)
endif()

View File

@ -1,30 +1,47 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#include <set>
#include <common/settings_input.h>
#include <jni.h>
#include "common/android/android_common.h"
#include "common/android/id_cache.h"
#include "input_common/drivers/android.h" #include "input_common/drivers/android.h"
namespace InputCommon { namespace InputCommon {
Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {} Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {}
void Android::RegisterController(std::size_t controller_number) { void Android::RegisterController(jobject j_input_device) {
PreSetController(GetIdentifier(controller_number)); auto env = Common::Android::GetEnvForThread();
const std::string guid = Common::Android::GetJString(
env, static_cast<jstring>(
env->CallObjectMethod(j_input_device, Common::Android::GetYuzuDeviceGetGUID())));
const s32 port = env->CallIntMethod(j_input_device, Common::Android::GetYuzuDeviceGetPort());
const auto identifier = GetIdentifier(guid, static_cast<size_t>(port));
PreSetController(identifier);
if (input_devices.find(identifier) != input_devices.end()) {
env->DeleteGlobalRef(input_devices[identifier]);
}
auto new_device = env->NewGlobalRef(j_input_device);
input_devices[identifier] = new_device;
} }
void Android::SetButtonState(std::size_t controller_number, int button_id, bool value) { void Android::SetButtonState(std::string guid, size_t port, int button_id, bool value) {
const auto identifier = GetIdentifier(controller_number); const auto identifier = GetIdentifier(guid, port);
SetButton(identifier, button_id, value); SetButton(identifier, button_id, value);
} }
void Android::SetAxisState(std::size_t controller_number, int axis_id, float value) { void Android::SetAxisPosition(std::string guid, size_t port, int axis_id, float value) {
const auto identifier = GetIdentifier(controller_number); const auto identifier = GetIdentifier(guid, port);
SetAxis(identifier, axis_id, value); SetAxis(identifier, axis_id, value);
} }
void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x, void Android::SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,
float gyro_y, float gyro_z, float accel_x, float accel_y, float gyro_y, float gyro_z, float accel_x, float accel_y,
float accel_z) { float accel_z) {
const auto identifier = GetIdentifier(controller_number); const auto identifier = GetIdentifier(guid, port);
const BasicMotion motion_data{ const BasicMotion motion_data{
.gyro_x = gyro_x, .gyro_x = gyro_x,
.gyro_y = gyro_y, .gyro_y = gyro_y,
@ -37,10 +54,295 @@ void Android::SetMotionState(std::size_t controller_number, u64 delta_timestamp,
SetMotion(identifier, 0, motion_data); SetMotion(identifier, 0, motion_data);
} }
PadIdentifier Android::GetIdentifier(std::size_t controller_number) const { Common::Input::DriverResult Android::SetVibration(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::VibrationStatus& vibration) {
auto device = input_devices.find(identifier);
if (device != input_devices.end()) {
Common::Android::RunJNIOnFiber<void>([&](JNIEnv* env) {
float average_intensity =
static_cast<float>((vibration.high_amplitude + vibration.low_amplitude) / 2.0);
env->CallVoidMethod(device->second, Common::Android::GetYuzuDeviceVibrate(),
average_intensity);
});
return Common::Input::DriverResult::Success;
}
return Common::Input::DriverResult::NotSupported;
}
bool Android::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
auto device = input_devices.find(identifier);
if (device != input_devices.end()) {
return Common::Android::RunJNIOnFiber<bool>([&](JNIEnv* env) {
return static_cast<bool>(env->CallBooleanMethod(
device->second, Common::Android::GetYuzuDeviceGetSupportsVibration()));
});
}
return false;
}
std::vector<Common::ParamPackage> Android::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
auto env = Common::Android::GetEnvForThread();
for (const auto& [key, value] : input_devices) {
auto name_object = static_cast<jstring>(
env->CallObjectMethod(value, Common::Android::GetYuzuDeviceGetName()));
const std::string name =
fmt::format("{} {}", Common::Android::GetJString(env, name_object), key.port);
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", std::move(name)},
{"guid", key.guid.RawString()},
{"port", std::to_string(key.port)},
});
}
return devices;
}
std::set<s32> Android::GetDeviceAxes(JNIEnv* env, jobject& j_device) const {
auto j_axes = static_cast<jobjectArray>(
env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceGetAxes()));
std::set<s32> axes;
for (int i = 0; i < env->GetArrayLength(j_axes); ++i) {
jobject axis = env->GetObjectArrayElement(j_axes, i);
axes.insert(env->GetIntField(axis, Common::Android::GetIntegerValueField()));
}
return axes;
}
Common::ParamPackage Android::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
int axis_y) const {
Common::ParamPackage params;
params.Set("engine", GetEngineName());
params.Set("port", static_cast<int>(identifier.port));
params.Set("guid", identifier.guid.RawString());
params.Set("axis_x", axis_x);
params.Set("axis_y", axis_y);
params.Set("offset_x", 0);
params.Set("offset_y", 0);
params.Set("invert_x", "+");
// Invert Y-Axis by default
params.Set("invert_y", "-");
return params;
}
Common::ParamPackage Android::BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis,
bool invert) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", static_cast<int>(identifier.port));
params.Set("guid", identifier.guid.RawString());
params.Set("axis", axis);
params.Set("threshold", "0.5");
params.Set("invert", invert ? "-" : "+");
return params;
}
Common::ParamPackage Android::BuildButtonParamPackageForButton(PadIdentifier identifier,
s32 button) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", static_cast<int>(identifier.port));
params.Set("guid", identifier.guid.RawString());
params.Set("button", button);
return params;
}
bool Android::MatchVID(Common::UUID device, const std::vector<std::string>& vids) const {
for (size_t i = 0; i < vids.size(); ++i) {
auto fucker = device.RawString();
if (fucker.find(vids[i]) != std::string::npos) {
return true;
}
}
return false;
}
AnalogMapping Android::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
auto identifier =
GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0)));
auto& j_device = input_devices[identifier];
if (j_device == nullptr) {
return {};
}
auto env = Common::Android::GetEnvForThread();
std::set<s32> axes = GetDeviceAxes(env, j_device);
if (axes.size() == 0) {
return {};
}
AnalogMapping mapping = {};
if (axes.find(AXIS_X) != axes.end() && axes.find(AXIS_Y) != axes.end()) {
mapping.insert_or_assign(Settings::NativeAnalog::LStick,
BuildParamPackageForAnalog(identifier, AXIS_X, AXIS_Y));
}
if (axes.find(AXIS_RX) != axes.end() && axes.find(AXIS_RY) != axes.end()) {
mapping.insert_or_assign(Settings::NativeAnalog::RStick,
BuildParamPackageForAnalog(identifier, AXIS_RX, AXIS_RY));
} else if (axes.find(AXIS_Z) != axes.end() && axes.find(AXIS_RZ) != axes.end()) {
mapping.insert_or_assign(Settings::NativeAnalog::RStick,
BuildParamPackageForAnalog(identifier, AXIS_Z, AXIS_RZ));
}
return mapping;
}
ButtonMapping Android::GetButtonMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
auto identifier =
GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0)));
auto& j_device = input_devices[identifier];
if (j_device == nullptr) {
return {};
}
auto env = Common::Android::GetEnvForThread();
jintArray j_keys = env->NewIntArray(static_cast<int>(keycode_ids.size()));
env->SetIntArrayRegion(j_keys, 0, static_cast<int>(keycode_ids.size()), keycode_ids.data());
auto j_has_keys_object = static_cast<jbooleanArray>(
env->CallObjectMethod(j_device, Common::Android::GetYuzuDeviceHasKeys(), j_keys));
jboolean isCopy = false;
jboolean* j_has_keys = env->GetBooleanArrayElements(j_has_keys_object, &isCopy);
std::set<s32> available_keys;
for (size_t i = 0; i < keycode_ids.size(); ++i) {
if (j_has_keys[i]) {
available_keys.insert(keycode_ids[i]);
}
}
// Some devices use axes instead of buttons for certain controls so we need all the axes here
std::set<s32> axes = GetDeviceAxes(env, j_device);
ButtonMapping mapping = {};
if (axes.find(AXIS_HAT_X) != axes.end() && axes.find(AXIS_HAT_Y) != axes.end()) {
mapping.insert_or_assign(Settings::NativeButton::DUp,
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, true));
mapping.insert_or_assign(Settings::NativeButton::DDown,
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, false));
mapping.insert_or_assign(Settings::NativeButton::DLeft,
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, true));
mapping.insert_or_assign(Settings::NativeButton::DRight,
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, false));
} else if (available_keys.find(KEYCODE_DPAD_UP) != available_keys.end() &&
available_keys.find(KEYCODE_DPAD_DOWN) != available_keys.end() &&
available_keys.find(KEYCODE_DPAD_LEFT) != available_keys.end() &&
available_keys.find(KEYCODE_DPAD_RIGHT) != available_keys.end()) {
mapping.insert_or_assign(Settings::NativeButton::DUp,
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_UP));
mapping.insert_or_assign(Settings::NativeButton::DDown,
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_DOWN));
mapping.insert_or_assign(Settings::NativeButton::DLeft,
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_LEFT));
mapping.insert_or_assign(Settings::NativeButton::DRight,
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_RIGHT));
}
if (axes.find(AXIS_LTRIGGER) != axes.end()) {
mapping.insert_or_assign(Settings::NativeButton::ZL, BuildAnalogParamPackageForButton(
identifier, AXIS_LTRIGGER, false));
} else if (available_keys.find(KEYCODE_BUTTON_L2) != available_keys.end()) {
mapping.insert_or_assign(Settings::NativeButton::ZL,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L2));
}
if (axes.find(AXIS_RTRIGGER) != axes.end()) {
mapping.insert_or_assign(Settings::NativeButton::ZR, BuildAnalogParamPackageForButton(
identifier, AXIS_RTRIGGER, false));
} else if (available_keys.find(KEYCODE_BUTTON_R2) != available_keys.end()) {
mapping.insert_or_assign(Settings::NativeButton::ZR,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R2));
}
if (available_keys.find(KEYCODE_BUTTON_A) != available_keys.end()) {
if (MatchVID(identifier.guid, flipped_ab_vids)) {
mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_A));
} else {
mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_A));
}
}
if (available_keys.find(KEYCODE_BUTTON_B) != available_keys.end()) {
if (MatchVID(identifier.guid, flipped_ab_vids)) {
mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_B));
} else {
mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_B));
}
}
if (available_keys.find(KEYCODE_BUTTON_X) != available_keys.end()) {
if (MatchVID(identifier.guid, flipped_xy_vids)) {
mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_X));
} else {
mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_X));
}
}
if (available_keys.find(KEYCODE_BUTTON_Y) != available_keys.end()) {
if (MatchVID(identifier.guid, flipped_xy_vids)) {
mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_Y));
} else {
mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton(
identifier, KEYCODE_BUTTON_Y));
}
}
if (available_keys.find(KEYCODE_BUTTON_L1) != available_keys.end()) {
mapping.insert_or_assign(Settings::NativeButton::L,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L1));
}
if (available_keys.find(KEYCODE_BUTTON_R1) != available_keys.end()) {
mapping.insert_or_assign(Settings::NativeButton::R,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R1));
}
if (available_keys.find(KEYCODE_BUTTON_THUMBL) != available_keys.end()) {
mapping.insert_or_assign(
Settings::NativeButton::LStick,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBL));
}
if (available_keys.find(KEYCODE_BUTTON_THUMBR) != available_keys.end()) {
mapping.insert_or_assign(
Settings::NativeButton::RStick,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBR));
}
if (available_keys.find(KEYCODE_BUTTON_START) != available_keys.end()) {
mapping.insert_or_assign(
Settings::NativeButton::Plus,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_START));
}
if (available_keys.find(KEYCODE_BUTTON_SELECT) != available_keys.end()) {
mapping.insert_or_assign(
Settings::NativeButton::Minus,
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_SELECT));
}
return mapping;
}
Common::Input::ButtonNames Android::GetUIName(
[[maybe_unused]] const Common::ParamPackage& params) const {
return Common::Input::ButtonNames::Value;
}
PadIdentifier Android::GetIdentifier(const std::string& guid, size_t port) const {
return { return {
.guid = Common::UUID{}, .guid = Common::UUID{guid},
.port = controller_number, .port = port,
.pad = 0, .pad = 0,
}; };
} }

View File

@ -3,6 +3,8 @@
#pragma once #pragma once
#include <set>
#include <jni.h>
#include "input_common/input_engine.h" #include "input_common/input_engine.h"
namespace InputCommon { namespace InputCommon {
@ -15,40 +17,121 @@ public:
explicit Android(std::string input_engine_); explicit Android(std::string input_engine_);
/** /**
* Registers controller number to accept new inputs * Registers controller number to accept new inputs.
* @param controller_number the controller number that will take this action * @param j_input_device YuzuInputDevice object from the Android frontend to register.
*/ */
void RegisterController(std::size_t controller_number); void RegisterController(jobject j_input_device);
/** /**
* Sets the status of all buttons bound with the key to pressed * Sets the status of a button on a specific controller.
* @param controller_number the controller number that will take this action * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param button_id the id of the button * @param port Port determined by controller connection order.
* @param value indicates if the button is pressed or not * @param button_id The Android Keycode corresponding to this event.
* @param value Whether the button is pressed or not.
*/ */
void SetButtonState(std::size_t controller_number, int button_id, bool value); void SetButtonState(std::string guid, size_t port, int button_id, bool value);
/** /**
* Sets the status of a analog input to a specific player index * Sets the status of an axis on a specific controller.
* @param controller_number the controller number that will take this action * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param axis_id the id of the axis to move * @param port Port determined by controller connection order.
* @param value the analog position of the axis * @param axis_id The Android axis ID corresponding to this event.
* @param value Value along the given axis.
*/ */
void SetAxisState(std::size_t controller_number, int axis_id, float value); void SetAxisPosition(std::string guid, size_t port, int axis_id, float value);
/** /**
* Sets the status of the motion sensor to a specific player index * Sets the status of the motion sensor on a specific controller
* @param controller_number the controller number that will take this action * @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
* @param delta_timestamp time passed since last reading * @param port Port determined by controller connection order.
* @param gyro_x,gyro_y,gyro_z the gyro sensor readings * @param delta_timestamp Time passed since the last read.
* @param accel_x,accel_y,accel_z the accelerometer reading * @param gyro_x,gyro_y,gyro_z Gyro sensor readings.
* @param accel_x,accel_y,accel_z Accelerometer sensor readings.
*/ */
void SetMotionState(std::size_t controller_number, u64 delta_timestamp, float gyro_x, void SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,
float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z); float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z);
Common::Input::DriverResult SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
std::vector<Common::ParamPackage> GetInputDevices() const override;
/**
* Gets the axes reported by the YuzuInputDevice.
* @param env JNI environment pointer.
* @param j_device YuzuInputDevice from the Android frontend.
* @return Set of the axes reported by the underlying Android InputDevice
*/
std::set<s32> GetDeviceAxes(JNIEnv* env, jobject& j_device) const;
Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
int axis_y) const;
Common::ParamPackage BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis,
bool invert) const;
Common::ParamPackage BuildButtonParamPackageForButton(PadIdentifier identifier,
s32 button) const;
bool MatchVID(Common::UUID device, const std::vector<std::string>& vids) const;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
private: private:
std::unordered_map<PadIdentifier, jobject> input_devices;
/// Returns the correct identifier corresponding to the player index /// Returns the correct identifier corresponding to the player index
PadIdentifier GetIdentifier(std::size_t controller_number) const; PadIdentifier GetIdentifier(const std::string& guid, size_t port) const;
static constexpr s32 AXIS_X = 0;
static constexpr s32 AXIS_Y = 1;
static constexpr s32 AXIS_Z = 11;
static constexpr s32 AXIS_RX = 12;
static constexpr s32 AXIS_RY = 13;
static constexpr s32 AXIS_RZ = 14;
static constexpr s32 AXIS_HAT_X = 15;
static constexpr s32 AXIS_HAT_Y = 16;
static constexpr s32 AXIS_LTRIGGER = 17;
static constexpr s32 AXIS_RTRIGGER = 18;
static constexpr s32 KEYCODE_DPAD_UP = 19;
static constexpr s32 KEYCODE_DPAD_DOWN = 20;
static constexpr s32 KEYCODE_DPAD_LEFT = 21;
static constexpr s32 KEYCODE_DPAD_RIGHT = 22;
static constexpr s32 KEYCODE_BUTTON_A = 96;
static constexpr s32 KEYCODE_BUTTON_B = 97;
static constexpr s32 KEYCODE_BUTTON_X = 99;
static constexpr s32 KEYCODE_BUTTON_Y = 100;
static constexpr s32 KEYCODE_BUTTON_L1 = 102;
static constexpr s32 KEYCODE_BUTTON_R1 = 103;
static constexpr s32 KEYCODE_BUTTON_L2 = 104;
static constexpr s32 KEYCODE_BUTTON_R2 = 105;
static constexpr s32 KEYCODE_BUTTON_THUMBL = 106;
static constexpr s32 KEYCODE_BUTTON_THUMBR = 107;
static constexpr s32 KEYCODE_BUTTON_START = 108;
static constexpr s32 KEYCODE_BUTTON_SELECT = 109;
const std::vector<s32> keycode_ids{
KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT,
KEYCODE_BUTTON_A, KEYCODE_BUTTON_B, KEYCODE_BUTTON_X, KEYCODE_BUTTON_Y,
KEYCODE_BUTTON_L1, KEYCODE_BUTTON_R1, KEYCODE_BUTTON_L2, KEYCODE_BUTTON_R2,
KEYCODE_BUTTON_THUMBL, KEYCODE_BUTTON_THUMBR, KEYCODE_BUTTON_START, KEYCODE_BUTTON_SELECT,
};
const std::string sony_vid{"054c"};
const std::string nintendo_vid{"057e"};
const std::string razer_vid{"1532"};
const std::string redmagic_vid{"3537"};
const std::string backbone_labs_vid{"358a"};
const std::vector<std::string> flipped_ab_vids{sony_vid, nintendo_vid, razer_vid, redmagic_vid,
backbone_labs_vid};
const std::vector<std::string> flipped_xy_vids{sony_vid, razer_vid, redmagic_vid,
backbone_labs_vid};
}; };
} // namespace InputCommon } // namespace InputCommon

View File

@ -4,7 +4,6 @@
#include <memory> #include <memory>
#include "common/input.h" #include "common/input.h"
#include "common/param_package.h" #include "common/param_package.h"
#include "input_common/drivers/android.h"
#include "input_common/drivers/camera.h" #include "input_common/drivers/camera.h"
#include "input_common/drivers/keyboard.h" #include "input_common/drivers/keyboard.h"
#include "input_common/drivers/mouse.h" #include "input_common/drivers/mouse.h"
@ -28,6 +27,10 @@
#include "input_common/drivers/sdl_driver.h" #include "input_common/drivers/sdl_driver.h"
#endif #endif
#ifdef ANDROID
#include "input_common/drivers/android.h"
#endif
namespace InputCommon { namespace InputCommon {
/// Dummy engine to get periodic updates /// Dummy engine to get periodic updates
@ -79,7 +82,9 @@ struct InputSubsystem::Impl {
RegisterEngine("cemuhookudp", udp_client); RegisterEngine("cemuhookudp", udp_client);
RegisterEngine("tas", tas_input); RegisterEngine("tas", tas_input);
RegisterEngine("camera", camera); RegisterEngine("camera", camera);
#ifdef ANDROID
RegisterEngine("android", android); RegisterEngine("android", android);
#endif
RegisterEngine("virtual_amiibo", virtual_amiibo); RegisterEngine("virtual_amiibo", virtual_amiibo);
RegisterEngine("virtual_gamepad", virtual_gamepad); RegisterEngine("virtual_gamepad", virtual_gamepad);
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
@ -111,7 +116,9 @@ struct InputSubsystem::Impl {
UnregisterEngine(udp_client); UnregisterEngine(udp_client);
UnregisterEngine(tas_input); UnregisterEngine(tas_input);
UnregisterEngine(camera); UnregisterEngine(camera);
#ifdef ANDROID
UnregisterEngine(android); UnregisterEngine(android);
#endif
UnregisterEngine(virtual_amiibo); UnregisterEngine(virtual_amiibo);
UnregisterEngine(virtual_gamepad); UnregisterEngine(virtual_gamepad);
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
@ -128,12 +135,16 @@ struct InputSubsystem::Impl {
Common::ParamPackage{{"display", "Any"}, {"engine", "any"}}, Common::ParamPackage{{"display", "Any"}, {"engine", "any"}},
}; };
#ifndef ANDROID
auto keyboard_devices = keyboard->GetInputDevices(); auto keyboard_devices = keyboard->GetInputDevices();
devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end()); devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end());
auto mouse_devices = mouse->GetInputDevices(); auto mouse_devices = mouse->GetInputDevices();
devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end()); devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end());
#endif
#ifdef ANDROID
auto android_devices = android->GetInputDevices(); auto android_devices = android->GetInputDevices();
devices.insert(devices.end(), android_devices.begin(), android_devices.end()); devices.insert(devices.end(), android_devices.begin(), android_devices.end());
#endif
#ifdef HAVE_LIBUSB #ifdef HAVE_LIBUSB
auto gcadapter_devices = gcadapter->GetInputDevices(); auto gcadapter_devices = gcadapter->GetInputDevices();
devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end()); devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end());
@ -162,9 +173,11 @@ struct InputSubsystem::Impl {
if (engine == mouse->GetEngineName()) { if (engine == mouse->GetEngineName()) {
return mouse; return mouse;
} }
#ifdef ANDROID
if (engine == android->GetEngineName()) { if (engine == android->GetEngineName()) {
return android; return android;
} }
#endif
#ifdef HAVE_LIBUSB #ifdef HAVE_LIBUSB
if (engine == gcadapter->GetEngineName()) { if (engine == gcadapter->GetEngineName()) {
return gcadapter; return gcadapter;
@ -245,9 +258,11 @@ struct InputSubsystem::Impl {
if (engine == mouse->GetEngineName()) { if (engine == mouse->GetEngineName()) {
return true; return true;
} }
#ifdef ANDROID
if (engine == android->GetEngineName()) { if (engine == android->GetEngineName()) {
return true; return true;
} }
#endif
#ifdef HAVE_LIBUSB #ifdef HAVE_LIBUSB
if (engine == gcadapter->GetEngineName()) { if (engine == gcadapter->GetEngineName()) {
return true; return true;
@ -276,7 +291,9 @@ struct InputSubsystem::Impl {
void BeginConfiguration() { void BeginConfiguration() {
keyboard->BeginConfiguration(); keyboard->BeginConfiguration();
mouse->BeginConfiguration(); mouse->BeginConfiguration();
#ifdef ANDROID
android->BeginConfiguration(); android->BeginConfiguration();
#endif
#ifdef HAVE_LIBUSB #ifdef HAVE_LIBUSB
gcadapter->BeginConfiguration(); gcadapter->BeginConfiguration();
#endif #endif
@ -290,7 +307,9 @@ struct InputSubsystem::Impl {
void EndConfiguration() { void EndConfiguration() {
keyboard->EndConfiguration(); keyboard->EndConfiguration();
mouse->EndConfiguration(); mouse->EndConfiguration();
#ifdef ANDROID
android->EndConfiguration(); android->EndConfiguration();
#endif
#ifdef HAVE_LIBUSB #ifdef HAVE_LIBUSB
gcadapter->EndConfiguration(); gcadapter->EndConfiguration();
#endif #endif
@ -321,7 +340,6 @@ struct InputSubsystem::Impl {
std::shared_ptr<TasInput::Tas> tas_input; std::shared_ptr<TasInput::Tas> tas_input;
std::shared_ptr<CemuhookUDP::UDPClient> udp_client; std::shared_ptr<CemuhookUDP::UDPClient> udp_client;
std::shared_ptr<Camera> camera; std::shared_ptr<Camera> camera;
std::shared_ptr<Android> android;
std::shared_ptr<VirtualAmiibo> virtual_amiibo; std::shared_ptr<VirtualAmiibo> virtual_amiibo;
std::shared_ptr<VirtualGamepad> virtual_gamepad; std::shared_ptr<VirtualGamepad> virtual_gamepad;
@ -333,6 +351,10 @@ struct InputSubsystem::Impl {
std::shared_ptr<SDLDriver> sdl; std::shared_ptr<SDLDriver> sdl;
std::shared_ptr<Joycons> joycon; std::shared_ptr<Joycons> joycon;
#endif #endif
#ifdef ANDROID
std::shared_ptr<Android> android;
#endif
}; };
InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {} InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
@ -387,6 +409,7 @@ const Camera* InputSubsystem::GetCamera() const {
return impl->camera.get(); return impl->camera.get();
} }
#ifdef ANDROID
Android* InputSubsystem::GetAndroid() { Android* InputSubsystem::GetAndroid() {
return impl->android.get(); return impl->android.get();
} }
@ -394,6 +417,7 @@ Android* InputSubsystem::GetAndroid() {
const Android* InputSubsystem::GetAndroid() const { const Android* InputSubsystem::GetAndroid() const {
return impl->android.get(); return impl->android.get();
} }
#endif
VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() { VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() {
return impl->virtual_amiibo.get(); return impl->virtual_amiibo.get();

View File

@ -90,6 +90,7 @@ void QtConfig::ReadQtPlayerValues(const std::size_t player_index) {
if (profile_name.empty()) { if (profile_name.empty()) {
// Use the global input config // Use the global input config
player = Settings::values.players.GetValue(true)[player_index]; player = Settings::values.players.GetValue(true)[player_index];
player.profile_name = "";
return; return;
} }
} }

Some files were not shown because too many files have changed in this diff Show More