feat(Android): Add DSU client server configuration UI and Wiimote DSU profile application.

This commit is contained in:
agamairi 2026-03-10 22:31:23 -04:00
parent 30a735aa6f
commit 42f04ae536
14 changed files with 546 additions and 12 deletions

View 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`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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`

View File

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

View File

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

View File

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

View File

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