mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-03-18 11:23:38 +00:00
Merge fd4b26d907 into 3564a256bf
This commit is contained in:
commit
109a1b0134
42
Data/Sys/Profiles/Wiimote/DSU Controller.ini
Normal file
42
Data/Sys/Profiles/Wiimote/DSU Controller.ini
Normal file
@ -0,0 +1,42 @@
|
||||
[Profile]
|
||||
Buttons/A = Cross
|
||||
Buttons/B = Square
|
||||
Buttons/1 = Triangle
|
||||
Buttons/2 = Circle
|
||||
Buttons/- = Share
|
||||
Buttons/+ = Options
|
||||
Buttons/Home = PS
|
||||
IR/Up = `Cursor Y-`
|
||||
IR/Down = `Cursor Y+`
|
||||
IR/Left = `Cursor X-`
|
||||
IR/Right = `Cursor X+`
|
||||
Tilt/Modifier/Range = 50.0
|
||||
Shake/X = `Middle Click`
|
||||
Shake/Y = `Middle Click`
|
||||
Shake/Z = `Middle Click`
|
||||
IMUAccelerometer/Up = `Accel Up`
|
||||
IMUAccelerometer/Down = `Accel Down`
|
||||
IMUAccelerometer/Left = `Accel Left`
|
||||
IMUAccelerometer/Right = `Accel Right`
|
||||
IMUAccelerometer/Forward = `Accel Forward`
|
||||
IMUAccelerometer/Backward = `Accel Backward`
|
||||
IMUGyroscope/Pitch Up = `Gyro Pitch Up`
|
||||
IMUGyroscope/Pitch Down = `Gyro Pitch Down`
|
||||
IMUGyroscope/Roll Left = `Gyro Roll Left`
|
||||
IMUGyroscope/Roll Right = `Gyro Roll Right`
|
||||
IMUGyroscope/Yaw Left = `Gyro Yaw Left`
|
||||
IMUGyroscope/Yaw Right = `Gyro Yaw Right`
|
||||
Nunchuk/Stick/Modifier/Range = 50.0
|
||||
Nunchuk/Tilt/Modifier/Range = 50.0
|
||||
Classic/Left Stick/Modifier/Range = 50.0
|
||||
Classic/Right Stick/Modifier/Range = 50.0
|
||||
Guitar/Stick/Modifier/Range = 50.0
|
||||
Drums/Stick/Modifier/Range = 50.0
|
||||
Turntable/Stick/Modifier/Range = 50.0
|
||||
uDraw/Stylus/Modifier/Range = 50.0
|
||||
Drawsome/Stylus/Modifier/Range = 50.0
|
||||
Rumble/Motor = `Motor 0`
|
||||
D-Pad/Up = `Pad N`
|
||||
D-Pad/Down = `Pad S`
|
||||
D-Pad/Left = `Pad W`
|
||||
D-Pad/Right = `Pad E`
|
||||
@ -671,6 +671,12 @@ enum class BooleanSetting(
|
||||
"ButtonLatchingNunchukZ",
|
||||
false
|
||||
),
|
||||
DSUCLIENT_SERVERS_ENABLED(
|
||||
Settings.FILE_DSUCLIENT,
|
||||
Settings.SECTION_DSUCLIENT_SERVER,
|
||||
"Enabled",
|
||||
false
|
||||
),
|
||||
SYSCONF_SCREENSAVER(Settings.FILE_SYSCONF, "IPL", "SSV", false),
|
||||
SYSCONF_WIDESCREEN(Settings.FILE_SYSCONF, "IPL", "AR", true),
|
||||
SYSCONF_PROGRESSIVE_SCAN(Settings.FILE_SYSCONF, "IPL", "PGS", true),
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.settings.model
|
||||
|
||||
data class DsuClientServerEntry(
|
||||
val description: String,
|
||||
val address: String,
|
||||
val port: Int
|
||||
) {
|
||||
fun toConfigEntry(): String = "$description:$address:$port"
|
||||
}
|
||||
|
||||
object DsuClientServerEntries {
|
||||
private const val MAX_HOST_LENGTH = 253
|
||||
private const val MAX_PORT = 65535
|
||||
|
||||
private val IPV4_REGEX =
|
||||
Regex("^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$")
|
||||
private val HOST_LABEL_REGEX =
|
||||
Regex("^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$")
|
||||
|
||||
fun parseConfigEntries(configValue: String): List<DsuClientServerEntry> {
|
||||
return configValue.split(';').mapNotNull(::parseEntry)
|
||||
}
|
||||
|
||||
fun formatConfigEntries(entries: List<DsuClientServerEntry>): String {
|
||||
if (entries.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
return entries.joinToString(";") { it.toConfigEntry() } + ";"
|
||||
}
|
||||
|
||||
fun parsePort(portValue: String): Int? {
|
||||
val port = portValue.toIntOrNull() ?: return null
|
||||
return if (isValidPort(port)) port else null
|
||||
}
|
||||
|
||||
fun isValidDescription(description: String): Boolean {
|
||||
return !description.contains(':') && !description.contains(';')
|
||||
}
|
||||
|
||||
fun isValidAddress(address: String): Boolean {
|
||||
if (address.isEmpty() || address.length > MAX_HOST_LENGTH) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (address.contains(':') || address.contains(';') || address.contains(' ')) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (IPV4_REGEX.matches(address)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return address.split('.')
|
||||
.all { label -> HOST_LABEL_REGEX.matches(label) }
|
||||
}
|
||||
|
||||
fun isValidPort(port: Int): Boolean {
|
||||
return port in 1..MAX_PORT
|
||||
}
|
||||
|
||||
private fun parseEntry(entry: String): DsuClientServerEntry? {
|
||||
if (entry.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val firstColon = entry.indexOf(':')
|
||||
if (firstColon == -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
val secondColon = entry.indexOf(':', firstColon + 1)
|
||||
if (secondColon == -1 || entry.indexOf(':', secondColon + 1) != -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
val description = entry.substring(0, firstColon).trim()
|
||||
val address = entry.substring(firstColon + 1, secondColon).trim()
|
||||
val port = parsePort(entry.substring(secondColon + 1).trim()) ?: return null
|
||||
|
||||
if (!isValidDescription(description) || !isValidAddress(address)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return DsuClientServerEntry(description, address, port)
|
||||
}
|
||||
}
|
||||
@ -123,6 +123,8 @@ class Settings : Closeable {
|
||||
const val SECTION_EMULATED_USB_DEVICES = "EmulatedUSBDevices"
|
||||
const val SECTION_STEREOSCOPY = "Stereoscopy"
|
||||
const val SECTION_ANALYTICS = "Analytics"
|
||||
const val FILE_DSUCLIENT = "DualShockUDPClient"
|
||||
const val SECTION_DSUCLIENT_SERVER = "Server"
|
||||
const val SECTION_ACHIEVEMENTS = "Achievements"
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,6 +98,12 @@ enum class StringSetting(
|
||||
Settings.SECTION_ACHIEVEMENTS,
|
||||
"ApiToken",
|
||||
""
|
||||
),
|
||||
DSUCLIENT_SERVERS(
|
||||
Settings.FILE_DSUCLIENT,
|
||||
Settings.SECTION_DSUCLIENT_SERVER,
|
||||
"Entries",
|
||||
""
|
||||
);
|
||||
|
||||
override val isOverridden: Boolean
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.settings.ui
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.text.InputType
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.DsuClientServerEntries
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.DsuClientServerEntry
|
||||
|
||||
object DsuClientAddServerDialog {
|
||||
private const val DEFAULT_DESCRIPTION = "DSU Server"
|
||||
private const val DEFAULT_ADDRESS = "127.0.0.1"
|
||||
private const val DEFAULT_PORT = "26760"
|
||||
|
||||
fun show(
|
||||
context: Context,
|
||||
existingEntry: DsuClientServerEntry? = null,
|
||||
onServerEdited: (DsuClientServerEntry) -> Unit
|
||||
) {
|
||||
val layout = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
val padding = (16 * context.resources.displayMetrics.density).toInt()
|
||||
setPadding(padding, padding, padding, 0)
|
||||
}
|
||||
|
||||
val descriptionInput = EditText(context).apply {
|
||||
hint = context.getString(R.string.dsu_client_server_description)
|
||||
inputType = InputType.TYPE_CLASS_TEXT
|
||||
setText(existingEntry?.description ?: DEFAULT_DESCRIPTION)
|
||||
}
|
||||
layout.addView(descriptionInput)
|
||||
|
||||
val addressInput = EditText(context).apply {
|
||||
hint = context.getString(R.string.dsu_client_server_address)
|
||||
inputType = InputType.TYPE_CLASS_TEXT
|
||||
setText(existingEntry?.address ?: DEFAULT_ADDRESS)
|
||||
}
|
||||
layout.addView(addressInput)
|
||||
|
||||
val portInput = EditText(context).apply {
|
||||
hint = context.getString(R.string.dsu_client_server_port)
|
||||
setText(existingEntry?.port?.toString() ?: DEFAULT_PORT)
|
||||
inputType = InputType.TYPE_CLASS_NUMBER
|
||||
}
|
||||
layout.addView(portInput)
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(
|
||||
if (existingEntry == null) {
|
||||
R.string.dsu_client_add_server_title
|
||||
} else {
|
||||
R.string.dsu_client_edit_server_title
|
||||
}
|
||||
)
|
||||
.setView(layout)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
descriptionInput.error = null
|
||||
addressInput.error = null
|
||||
portInput.error = null
|
||||
|
||||
val descriptionText = descriptionInput.text.toString().trim()
|
||||
val description = if (descriptionText.isEmpty()) DEFAULT_DESCRIPTION else descriptionText
|
||||
val address = addressInput.text.toString().trim()
|
||||
val port = DsuClientServerEntries.parsePort(portInput.text.toString().trim())
|
||||
|
||||
var valid = true
|
||||
|
||||
if (!DsuClientServerEntries.isValidDescription(description)) {
|
||||
descriptionInput.error = context.getString(R.string.dsu_client_invalid_description)
|
||||
valid = false
|
||||
}
|
||||
|
||||
if (!DsuClientServerEntries.isValidAddress(address)) {
|
||||
addressInput.error = context.getString(R.string.dsu_client_invalid_address)
|
||||
valid = false
|
||||
}
|
||||
|
||||
if (port == null) {
|
||||
portInput.error = context.getString(R.string.dsu_client_invalid_port)
|
||||
valid = false
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
onServerEdited(DsuClientServerEntry(description, address, requireNotNull(port)))
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,6 +52,7 @@ enum class MenuTag {
|
||||
WIIMOTE_MOTION_INPUT_2("wiimote_motion_input", 1),
|
||||
WIIMOTE_MOTION_INPUT_3("wiimote_motion_input", 2),
|
||||
WIIMOTE_MOTION_INPUT_4("wiimote_motion_input", 3),
|
||||
CONFIG_DSU_CLIENT("config_dsu_client"),
|
||||
GPU_DRIVERS("gpu_drivers");
|
||||
|
||||
var tag: String
|
||||
|
||||
@ -270,6 +270,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
titles[MenuTag.CONFIG_WII] = R.string.wii_submenu
|
||||
titles[MenuTag.CONFIG_ACHIEVEMENTS] = R.string.achievements_submenu
|
||||
titles[MenuTag.CONFIG_ADVANCED] = R.string.advanced_submenu
|
||||
titles[MenuTag.CONFIG_DSU_CLIENT] = R.string.dsu_client_submenu
|
||||
titles[MenuTag.DEBUG] = R.string.debug_submenu
|
||||
titles[MenuTag.GRAPHICS] = R.string.graphics_settings
|
||||
titles[MenuTag.ENHANCEMENTS] = R.string.enhancements_submenu
|
||||
|
||||
@ -17,6 +17,7 @@ import kotlinx.coroutines.withContext
|
||||
import org.dolphinemu.dolphinemu.NativeLibrary
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
import org.dolphinemu.dolphinemu.activities.UserDataActivity
|
||||
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
|
||||
import org.dolphinemu.dolphinemu.features.input.model.ControlGroupEnabledSetting
|
||||
import org.dolphinemu.dolphinemu.features.input.model.InputMappingBooleanSetting
|
||||
import org.dolphinemu.dolphinemu.features.input.model.InputMappingDoubleSetting
|
||||
@ -34,6 +35,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.view.*
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.AchievementModel.logout
|
||||
import org.dolphinemu.dolphinemu.model.GpuDriverMetadata
|
||||
import org.dolphinemu.dolphinemu.utils.*
|
||||
import java.io.File
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
@ -112,6 +114,7 @@ class SettingsFragmentPresenter(
|
||||
MenuTag.CONFIG_WII -> addWiiSettings(sl)
|
||||
MenuTag.CONFIG_ACHIEVEMENTS -> addAchievementSettings(sl);
|
||||
MenuTag.CONFIG_ADVANCED -> addAdvancedSettings(sl)
|
||||
MenuTag.CONFIG_DSU_CLIENT -> addDSUClientSettings(sl)
|
||||
MenuTag.GRAPHICS -> addGraphicsSettings(sl)
|
||||
MenuTag.CONFIG_SERIALPORT1 -> addSerialPortSubSettings(sl, serialPort1Type)
|
||||
MenuTag.GCPAD_TYPE -> addGcPadSettings(sl)
|
||||
@ -202,6 +205,7 @@ class SettingsFragmentPresenter(
|
||||
sl.add(SubmenuSetting(context, R.string.wii_submenu, MenuTag.CONFIG_WII))
|
||||
sl.add(SubmenuSetting(context, R.string.achievements_submenu, MenuTag.CONFIG_ACHIEVEMENTS))
|
||||
sl.add(SubmenuSetting(context, R.string.advanced_submenu, MenuTag.CONFIG_ADVANCED))
|
||||
sl.add(SubmenuSetting(context, R.string.dsu_client_submenu, MenuTag.CONFIG_DSU_CLIENT))
|
||||
sl.add(SubmenuSetting(context, R.string.log_submenu, MenuTag.CONFIG_LOG))
|
||||
sl.add(SubmenuSetting(context, R.string.debug_submenu, MenuTag.DEBUG))
|
||||
sl.add(
|
||||
@ -294,6 +298,106 @@ class SettingsFragmentPresenter(
|
||||
)
|
||||
}
|
||||
|
||||
private fun addDSUClientSettings(sl: ArrayList<SettingsItem>) {
|
||||
sl.add(
|
||||
SwitchSetting(
|
||||
context,
|
||||
BooleanSetting.DSUCLIENT_SERVERS_ENABLED,
|
||||
R.string.dsu_client_enable,
|
||||
R.string.dsu_client_enable_description
|
||||
)
|
||||
)
|
||||
|
||||
sl.add(HeaderSetting(context, R.string.dsu_client_servers_header, 0))
|
||||
|
||||
val serverEntries =
|
||||
DsuClientServerEntries.parseConfigEntries(StringSetting.DSUCLIENT_SERVERS.string)
|
||||
|
||||
if (serverEntries.isEmpty()) {
|
||||
sl.add(
|
||||
HeaderSetting(
|
||||
context.getString(R.string.dsu_client_no_servers),
|
||||
""
|
||||
)
|
||||
)
|
||||
} else {
|
||||
for ((index, serverEntry) in serverEntries.withIndex()) {
|
||||
val displayName = if (serverEntry.description.isBlank()) {
|
||||
"${serverEntry.address}:${serverEntry.port}"
|
||||
} else {
|
||||
"${serverEntry.description} (${serverEntry.address}:${serverEntry.port})"
|
||||
}
|
||||
sl.add(HeaderSetting(displayName, ""))
|
||||
|
||||
val serverIndex = index
|
||||
sl.add(
|
||||
RunRunnable(
|
||||
context,
|
||||
R.string.dsu_client_edit_server,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
true
|
||||
) {
|
||||
DsuClientAddServerDialog.show(context, serverEntry) { editedServer ->
|
||||
val servers = getDsuClientServers()
|
||||
if (serverIndex in servers.indices) {
|
||||
servers[serverIndex] = editedServer
|
||||
saveDsuClientServers(servers)
|
||||
loadSettingsList()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sl.add(
|
||||
RunRunnable(
|
||||
context,
|
||||
R.string.dsu_client_remove_server,
|
||||
0,
|
||||
R.string.dsu_client_remove_server,
|
||||
0,
|
||||
true
|
||||
) {
|
||||
val servers = getDsuClientServers()
|
||||
if (serverIndex in servers.indices) {
|
||||
servers.removeAt(serverIndex)
|
||||
saveDsuClientServers(servers)
|
||||
loadSettingsList()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sl.add(
|
||||
RunRunnable(context, R.string.dsu_client_add_server, 0, 0, 0, true) {
|
||||
DsuClientAddServerDialog.show(context) { newServer ->
|
||||
val servers = getDsuClientServers()
|
||||
servers.add(newServer)
|
||||
saveDsuClientServers(servers)
|
||||
loadSettingsList()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDsuClientServers(): MutableList<DsuClientServerEntry> {
|
||||
return DsuClientServerEntries.parseConfigEntries(StringSetting.DSUCLIENT_SERVERS.string)
|
||||
.toMutableList()
|
||||
}
|
||||
|
||||
private fun saveDsuClientServers(serverEntries: List<DsuClientServerEntry>) {
|
||||
NativeConfig.setString(
|
||||
NativeConfig.LAYER_BASE,
|
||||
Settings.FILE_DSUCLIENT,
|
||||
Settings.SECTION_DSUCLIENT_SERVER,
|
||||
"Entries",
|
||||
DsuClientServerEntries.formatConfigEntries(serverEntries)
|
||||
)
|
||||
NativeConfig.save(NativeConfig.LAYER_BASE)
|
||||
}
|
||||
|
||||
private fun addInterfaceSettings(sl: ArrayList<SettingsItem>) {
|
||||
// Hide the orientation setting if the device only supports one orientation. Old devices which
|
||||
// support both portrait and landscape may report support for neither, so we use ==, not &&.
|
||||
@ -2592,6 +2696,19 @@ class SettingsFragmentPresenter(
|
||||
true
|
||||
) { fragmentView.showDialogFragment(ProfileDialog.create(menuTag)) })
|
||||
|
||||
if (menuTag.isWiimoteMenu) {
|
||||
sl.add(
|
||||
RunRunnable(
|
||||
context,
|
||||
R.string.input_dsu_apply_profile,
|
||||
R.string.input_dsu_apply_profile_description,
|
||||
0,
|
||||
0,
|
||||
true
|
||||
) { applyDsuWiimoteProfile(controller) }
|
||||
)
|
||||
}
|
||||
|
||||
updateOldControllerSettingsWarningVisibility(controller)
|
||||
}
|
||||
|
||||
@ -2717,6 +2834,61 @@ class SettingsFragmentPresenter(
|
||||
fragmentView.onControllerSettingsChanged()
|
||||
}
|
||||
|
||||
private fun applyDsuWiimoteProfile(controller: EmulatedController) {
|
||||
val dsuDevice = ControllerInterface.getAllDeviceStrings().firstOrNull {
|
||||
it.startsWith(DSU_DEVICE_PREFIX)
|
||||
}
|
||||
|
||||
if (dsuDevice == null) {
|
||||
fragmentView.showToastMessage(context.getString(R.string.input_dsu_apply_profile_no_device))
|
||||
return
|
||||
}
|
||||
|
||||
val profilePath = ensureDsuWiimoteProfile(controller)
|
||||
if (profilePath == null) {
|
||||
fragmentView.showToastMessage(context.getString(R.string.input_dsu_apply_profile_missing))
|
||||
return
|
||||
}
|
||||
|
||||
controller.setDefaultDevice(dsuDevice)
|
||||
controller.loadProfile(profilePath)
|
||||
enableWiimotePointInput(controller)
|
||||
fragmentView.onControllerSettingsChanged()
|
||||
fragmentView.showToastMessage(context.getString(R.string.input_dsu_apply_profile_success))
|
||||
}
|
||||
|
||||
private fun ensureDsuWiimoteProfile(controller: EmulatedController): String? {
|
||||
val sysProfilePath = controller.getSysProfileDirectoryPath() + DSU_WIIMOTE_PROFILE_FILE_NAME
|
||||
if (File(sysProfilePath).exists()) {
|
||||
return sysProfilePath
|
||||
}
|
||||
|
||||
val userProfileFile = File(controller.getUserProfileDirectoryPath(), DSU_WIIMOTE_PROFILE_FILE_NAME)
|
||||
userProfileFile.parentFile?.mkdirs()
|
||||
|
||||
return try {
|
||||
context.resources.openRawResource(R.raw.dsu_wiimote_profile).use { input ->
|
||||
userProfileFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
userProfileFile.path
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableWiimotePointInput(controller: EmulatedController) {
|
||||
val groupCount = controller.getGroupCount()
|
||||
for (i in 0 until groupCount) {
|
||||
val group = controller.getGroup(i)
|
||||
if (group.getGroupType() == ControlGroup.TYPE_IMU_CURSOR) {
|
||||
group.setEnabled(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAllLogTypes(value: Boolean) {
|
||||
val settings = fragmentView.settings
|
||||
|
||||
@ -2783,6 +2955,8 @@ class SettingsFragmentPresenter(
|
||||
companion object {
|
||||
const val ARG_CONTROLLER_TYPE = "controller_type"
|
||||
const val ARG_SERIALPORT1_TYPE = "serialport1_type"
|
||||
private const val DSU_DEVICE_PREFIX = "DSUClient/"
|
||||
private const val DSU_WIIMOTE_PROFILE_FILE_NAME = "DSU Controller.ini"
|
||||
|
||||
// Value obtained from LogLevel in Common/Logging/Log.h
|
||||
private fun getLogVerbosityEntries(): Int {
|
||||
|
||||
42
Source/Android/app/src/main/res/raw/dsu_wiimote_profile.ini
Normal file
42
Source/Android/app/src/main/res/raw/dsu_wiimote_profile.ini
Normal file
@ -0,0 +1,42 @@
|
||||
[Profile]
|
||||
Buttons/A = Cross
|
||||
Buttons/B = Square
|
||||
Buttons/1 = Triangle
|
||||
Buttons/2 = Circle
|
||||
Buttons/- = Share
|
||||
Buttons/+ = Options
|
||||
Buttons/Home = PS
|
||||
IR/Up = `Cursor Y-`
|
||||
IR/Down = `Cursor Y+`
|
||||
IR/Left = `Cursor X-`
|
||||
IR/Right = `Cursor X+`
|
||||
Tilt/Modifier/Range = 50.0
|
||||
Shake/X = `Middle Click`
|
||||
Shake/Y = `Middle Click`
|
||||
Shake/Z = `Middle Click`
|
||||
IMUAccelerometer/Up = `Accel Up`
|
||||
IMUAccelerometer/Down = `Accel Down`
|
||||
IMUAccelerometer/Left = `Accel Left`
|
||||
IMUAccelerometer/Right = `Accel Right`
|
||||
IMUAccelerometer/Forward = `Accel Forward`
|
||||
IMUAccelerometer/Backward = `Accel Backward`
|
||||
IMUGyroscope/Pitch Up = `Gyro Pitch Up`
|
||||
IMUGyroscope/Pitch Down = `Gyro Pitch Down`
|
||||
IMUGyroscope/Roll Left = `Gyro Roll Left`
|
||||
IMUGyroscope/Roll Right = `Gyro Roll Right`
|
||||
IMUGyroscope/Yaw Left = `Gyro Yaw Left`
|
||||
IMUGyroscope/Yaw Right = `Gyro Yaw Right`
|
||||
Nunchuk/Stick/Modifier/Range = 50.0
|
||||
Nunchuk/Tilt/Modifier/Range = 50.0
|
||||
Classic/Left Stick/Modifier/Range = 50.0
|
||||
Classic/Right Stick/Modifier/Range = 50.0
|
||||
Guitar/Stick/Modifier/Range = 50.0
|
||||
Drums/Stick/Modifier/Range = 50.0
|
||||
Turntable/Stick/Modifier/Range = 50.0
|
||||
uDraw/Stylus/Modifier/Range = 50.0
|
||||
Drawsome/Stylus/Modifier/Range = 50.0
|
||||
Rumble/Motor = `Motor 0`
|
||||
D-Pad/Up = `Pad N`
|
||||
D-Pad/Down = `Pad S`
|
||||
D-Pad/Left = `Pad W`
|
||||
D-Pad/Right = `Pad E`
|
||||
@ -41,6 +41,11 @@
|
||||
<string name="input_profile_load">Load</string>
|
||||
<string name="input_profile_save">Save</string>
|
||||
<string name="input_profile_delete">Delete</string>
|
||||
<string name="input_dsu_apply_profile">Apply DSU Preset</string>
|
||||
<string name="input_dsu_apply_profile_description">Loads the built-in DSU controller profile and selects the first connected DSUClient device.</string>
|
||||
<string name="input_dsu_apply_profile_no_device">No DSUClient device is currently available. Connect a DSU source first.</string>
|
||||
<string name="input_dsu_apply_profile_missing">Could not load the built-in DSU controller profile.</string>
|
||||
<string name="input_dsu_apply_profile_success">DSU controller preset applied.</string>
|
||||
<string name="input_profile_confirm_load">Do you want to discard your current controller settings and load the profile \"%1$s\"?</string>
|
||||
<string name="input_profile_confirm_save">Do you want to overwrite the profile \"%1$s\"?</string>
|
||||
<string name="input_profile_confirm_delete">Do you want to delete the profile \"%1$s\"?</string>
|
||||
@ -439,6 +444,25 @@
|
||||
<string name="debug_jitbranchoff">Jit Branch Disabled</string>
|
||||
<string name="debug_jitregistercacheoff">Jit Register Cache Disabled</string>
|
||||
|
||||
<!-- DSU Client Settings -->
|
||||
<string name="dsu_client_submenu">Alternate Input Sources</string>
|
||||
<string name="dsu_client_enable">Enable DSU Client</string>
|
||||
<string name="dsu_client_enable_description">Enable connecting to DSU (CemuHook) compatible input sources for motion and button data.</string>
|
||||
<string name="dsu_client_description">The DSU protocol enables the use of input and motion data from compatible sources, like phones running DS4Windows, DroidJoy, or other CemuHook-compatible apps.\n\nAdd a server below with its IP address and port to use it as an input source in controller configuration.</string>
|
||||
<string name="dsu_client_add_server">Add Server…</string>
|
||||
<string name="dsu_client_edit_server">Edit</string>
|
||||
<string name="dsu_client_remove_server">Remove</string>
|
||||
<string name="dsu_client_server_description">Description</string>
|
||||
<string name="dsu_client_server_address">Server Address (IP/host)</string>
|
||||
<string name="dsu_client_server_port">Server Port</string>
|
||||
<string name="dsu_client_no_servers">No DSU servers configured.</string>
|
||||
<string name="dsu_client_servers_header">Configured Servers</string>
|
||||
<string name="dsu_client_add_server_title">Add DSU Server</string>
|
||||
<string name="dsu_client_edit_server_title">Edit DSU Server</string>
|
||||
<string name="dsu_client_invalid_description">Description cannot contain colon or semicolon.</string>
|
||||
<string name="dsu_client_invalid_address">Enter a valid IPv4 address or hostname.</string>
|
||||
<string name="dsu_client_invalid_port">Enter a port from 1 to 65535.</string>
|
||||
|
||||
<!-- User Data -->
|
||||
<string name="user_data_submenu">User Data</string>
|
||||
<string name="user_data_old_location">Your user data (settings, saves, etc.) is stored in a location which will <b>not</b> be deleted when you uninstall the app:</string>
|
||||
@ -665,8 +689,8 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||
<!-- Misc -->
|
||||
<string name="enabled">Enabled</string>
|
||||
<string name="default_values">Default Values</string>
|
||||
<string name="slider_setting_value">%.0f%2$s</string>
|
||||
<string name="slider_setting_float_value">%.2f%2$s</string>
|
||||
<string name="slider_setting_value">%1$.0f%2$s</string>
|
||||
<string name="slider_setting_float_value">%1$.2f%2$s</string>
|
||||
<string name="disc_number">Disc %1$d</string>
|
||||
<string name="replug_gc_adapter">GameCube Adapter couldn\'t be opened. Please re-plug the device.</string>
|
||||
<string name="disabled_gc_overlay_notice">The selected GameCube controller is set to \"None\"</string>
|
||||
|
||||
@ -51,6 +51,10 @@ static Config::Location GetLocation(JNIEnv* env, jstring file, jstring section,
|
||||
{
|
||||
system = Config::System::Achievements;
|
||||
}
|
||||
else if (decoded_file == "DualShockUDPClient")
|
||||
{
|
||||
system = Config::System::DualShockUDPClient;
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT(false);
|
||||
|
||||
@ -31,7 +31,9 @@
|
||||
#if defined(USE_PIPES)
|
||||
#define CIFACE_USE_PIPES
|
||||
#endif
|
||||
#ifndef CIFACE_USE_DUALSHOCKUDPCLIENT
|
||||
#define CIFACE_USE_DUALSHOCKUDPCLIENT
|
||||
#endif
|
||||
#if defined(HAVE_SDL3)
|
||||
#define CIFACE_USE_SDL
|
||||
#endif
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
|
||||
#include <SFML/Network/SocketSelector.hpp>
|
||||
@ -195,6 +197,13 @@ struct Server
|
||||
SteadyClock::time_point m_disconnect_time = SteadyClock::now();
|
||||
};
|
||||
|
||||
struct ParsedServerEntry
|
||||
{
|
||||
std::string description;
|
||||
std::string address;
|
||||
u16 port;
|
||||
};
|
||||
|
||||
class InputBackend final : public ciface::InputBackend
|
||||
{
|
||||
public:
|
||||
@ -232,6 +241,39 @@ static bool IsSameController(const Proto::MessageType::PortInfo& a,
|
||||
std::tie(b.pad_id, b.pad_state, b.model, b.connection_type, b.pad_mac_address);
|
||||
}
|
||||
|
||||
static std::optional<ParsedServerEntry> ParseServerEntry(std::string_view entry)
|
||||
{
|
||||
if (entry.empty())
|
||||
return std::nullopt;
|
||||
|
||||
const size_t first_colon = entry.find(':');
|
||||
if (first_colon == std::string_view::npos)
|
||||
return std::nullopt;
|
||||
|
||||
const size_t second_colon = entry.find(':', first_colon + 1);
|
||||
|
||||
// We use "description:address:port". Reject entries that don't match exactly.
|
||||
if (second_colon == std::string_view::npos ||
|
||||
entry.find(':', second_colon + 1) != std::string_view::npos)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::string description{StripSpaces(entry.substr(0, first_colon))};
|
||||
const std::string address{
|
||||
StripSpaces(entry.substr(first_colon + 1, second_colon - first_colon - 1))};
|
||||
const std::string port_str{StripSpaces(entry.substr(second_colon + 1))};
|
||||
|
||||
u16 port = 0;
|
||||
if (!TryParse(port_str, &port) || port == 0)
|
||||
return std::nullopt;
|
||||
|
||||
if (address.empty())
|
||||
return std::nullopt;
|
||||
|
||||
return ParsedServerEntry{description, address, port};
|
||||
}
|
||||
|
||||
void InputBackend::HotplugThreadFunc()
|
||||
{
|
||||
Common::SetCurrentThreadName("DualShockUDPClient Hotplug Thread");
|
||||
@ -440,20 +482,19 @@ void InputBackend::ConfigChanged()
|
||||
const auto server_details = SplitString(servers_setting, ';');
|
||||
for (const auto& server_detail : server_details)
|
||||
{
|
||||
const auto server_info = SplitString(server_detail, ':');
|
||||
if (server_info.size() < 3)
|
||||
continue;
|
||||
|
||||
const std::string description = server_info[0];
|
||||
const std::string server_address = server_info[1];
|
||||
const auto port = std::stoi(server_info[2]);
|
||||
if (port >= std::numeric_limits<u16>::max())
|
||||
const auto parsed_server = ParseServerEntry(server_detail);
|
||||
if (!parsed_server.has_value())
|
||||
{
|
||||
if (!server_detail.empty())
|
||||
{
|
||||
WARN_LOG_FMT(CONTROLLERINTERFACE, "Ignoring invalid DSU server entry in config: \"{}\"",
|
||||
server_detail);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
u16 server_port = static_cast<u16>(port);
|
||||
|
||||
m_servers.emplace_back(description, server_address, server_port);
|
||||
m_servers.emplace_back(parsed_server->description, parsed_server->address,
|
||||
parsed_server->port);
|
||||
}
|
||||
Restart();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user