mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-09-18 10:50:05 +00:00
Merge pull request #14750 from tom-pratt/netplay-controller-mapping
Android: Netplay controller mapping
This commit is contained in:
+30
-1
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.runningFold
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.ControllerMapping
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
||||
@@ -87,6 +88,12 @@ class NetplaySession(
|
||||
)
|
||||
val players = _players.asSharedFlow().distinctUntilChanged()
|
||||
|
||||
private val _controllerMapping = MutableSharedFlow<ControllerMapping>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val controllerMapping = _controllerMapping.asSharedFlow().distinctUntilChanged()
|
||||
|
||||
private val _chatMessages = MutableSharedFlow<String>(
|
||||
extraBufferCapacity = 32,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
@@ -185,6 +192,11 @@ class NetplaySession(
|
||||
|
||||
fun reconnectTraversal() = nativeReconnectTraversal()
|
||||
|
||||
fun setControllerMapping(mapping: ControllerMapping) {
|
||||
nativeSetPadMapping(mapping.gamecubePorts.map { it?.pid ?: 0 }.toIntArray())
|
||||
nativeSetWiimoteMapping(mapping.wiiRemotes.map { it?.pid ?: 0 }.toIntArray())
|
||||
}
|
||||
|
||||
fun consumeBootSessionData(): Long {
|
||||
return bootSessionDataPointer.also {
|
||||
bootSessionDataPointer = 0
|
||||
@@ -278,6 +290,14 @@ class NetplaySession(
|
||||
|
||||
private external fun nativeReconnectTraversal()
|
||||
|
||||
private external fun nativeGetPadMapping(): IntArray
|
||||
|
||||
private external fun nativeGetWiimoteMapping(): IntArray
|
||||
|
||||
private external fun nativeSetPadMapping(mapping: IntArray)
|
||||
|
||||
private external fun nativeSetWiimoteMapping(mapping: IntArray)
|
||||
|
||||
// NetPlayUI callbacks
|
||||
|
||||
@Keep
|
||||
@@ -304,7 +324,16 @@ class NetplaySession(
|
||||
|
||||
@Keep
|
||||
fun onUpdate(players: Array<Player>) {
|
||||
_players.tryEmit(players.toList())
|
||||
val playersList = players.toList()
|
||||
_players.tryEmit(playersList)
|
||||
|
||||
fun findPlayer(playerId: Int) = playersList.find { it.pid == playerId }
|
||||
_controllerMapping.tryEmit(
|
||||
ControllerMapping(
|
||||
gamecubePorts = nativeGetPadMapping().map(::findPlayer).toList(),
|
||||
wiiRemotes = nativeGetWiimoteMapping().map(::findPlayer).toList(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Keep
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.netplay.model
|
||||
|
||||
data class ControllerMapping(
|
||||
val gamecubePorts: List<Player?>,
|
||||
val wiiRemotes: List<Player?>,
|
||||
) {
|
||||
fun withGamecubePort(port: Int, player: Player?) =
|
||||
copy(gamecubePorts = gamecubePorts.replace(port, player))
|
||||
|
||||
fun withWiiRemote(port: Int, player: Player?) =
|
||||
copy(wiiRemotes = wiiRemotes.replace(port, player))
|
||||
|
||||
companion object {
|
||||
fun emptyControllerMapping(): ControllerMapping = ControllerMapping(
|
||||
gamecubePorts = emptyList(),
|
||||
wiiRemotes = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> List<T>.replace(index: Int, value: T) =
|
||||
toMutableList().also { it[index] = value }
|
||||
+16
@@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.dolphinemu.dolphinemu.features.netplay.NetplaySession
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.ControllerMapping.Companion.emptyControllerMapping
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
|
||||
@@ -59,6 +60,9 @@ class NetplayViewModel(
|
||||
val players = netplaySession.players
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
val controllerMapping = netplaySession.controllerMapping
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyControllerMapping())
|
||||
|
||||
val messages = netplaySession.messages
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
@@ -168,6 +172,18 @@ class NetplayViewModel(
|
||||
netplaySession.changeGame(gameFile)
|
||||
}
|
||||
|
||||
fun setGamecubePort(portNumber: Int, player: Player?) {
|
||||
netplaySession.setControllerMapping(
|
||||
controllerMapping.value.withGamecubePort(portNumber, player)
|
||||
)
|
||||
}
|
||||
|
||||
fun setWiiRemote(remoteNumber: Int, player: Player?) {
|
||||
netplaySession.setControllerMapping(
|
||||
controllerMapping.value.withWiiRemote(remoteNumber, player)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLocalIp(): JoinAddress {
|
||||
val localIp = networkHelper.getLocalIpString()
|
||||
?: return JoinAddress.Unknown { _joinAddresses.value += JoinInfoType.LOCAL to getLocalIp() }
|
||||
|
||||
+3
@@ -73,6 +73,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
|
||||
saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value,
|
||||
gameDigestProgress = viewModel.gameDigestProgress.collectAsState().value,
|
||||
joinAddresses = viewModel.joinAddresses.collectAsState().value,
|
||||
controllerMapping = viewModel.controllerMapping.collectAsState().value,
|
||||
onGamecubePortChanged = viewModel::setGamecubePort,
|
||||
onWiiRemoteChanged = viewModel::setWiiRemote,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+270
-38
@@ -4,6 +4,7 @@ package org.dolphinemu.dolphinemu.features.netplay.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
@@ -20,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
@@ -38,7 +40,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Remove
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
@@ -79,10 +80,8 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -92,6 +91,7 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
@@ -100,6 +100,7 @@ import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.ControllerMapping
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.JoinAddress
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.JoinInfoType
|
||||
@@ -149,6 +150,9 @@ fun NetplayScreen(
|
||||
saveTransferProgress: SaveTransferProgress?,
|
||||
gameDigestProgress: GameDigestProgress?,
|
||||
joinAddresses: Map<JoinInfoType, JoinAddress>,
|
||||
controllerMapping: ControllerMapping,
|
||||
onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
onWiiRemoteChanged: (port: Int, player: Player?) -> Unit,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
@@ -179,6 +183,7 @@ fun NetplayScreen(
|
||||
// State which must live above the landscape/portrait split.
|
||||
var showChat by rememberSaveable { mutableStateOf(false) }
|
||||
var showGamePicker by rememberSaveable { mutableStateOf(false) }
|
||||
var showControllerMapping by rememberSaveable { mutableStateOf(false) }
|
||||
var selectedJoinInfoType by rememberSaveable {
|
||||
mutableStateOf(joinAddresses.keys.firstOrNull() ?: JoinInfoType.EXTERNAL)
|
||||
}
|
||||
@@ -202,6 +207,11 @@ fun NetplayScreen(
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = { showGamePicker = it },
|
||||
players = players,
|
||||
controllerMapping = controllerMapping,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiiRemoteChanged,
|
||||
showControllerMapping = showControllerMapping,
|
||||
onShowControllerMappingChanged = { showControllerMapping = it },
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
networkMode = networkMode,
|
||||
onNetworkModeChanged = onNetworkModeChanged,
|
||||
@@ -229,6 +239,11 @@ fun NetplayScreen(
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = { showGamePicker = it },
|
||||
players = players,
|
||||
controllerMapping = controllerMapping,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiiRemoteChanged,
|
||||
showControllerMapping = showControllerMapping,
|
||||
onShowControllerMappingChanged = { showControllerMapping = it },
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
networkMode = networkMode,
|
||||
onNetworkModeChanged = onNetworkModeChanged,
|
||||
@@ -368,6 +383,11 @@ private fun PortraitContent(
|
||||
showGamePicker: Boolean,
|
||||
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||
players: List<Player>,
|
||||
controllerMapping: ControllerMapping,
|
||||
onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
showControllerMapping: Boolean,
|
||||
onShowControllerMappingChanged: (Boolean) -> Unit,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
networkMode: NetworkMode,
|
||||
onNetworkModeChanged: (NetworkMode) -> Unit,
|
||||
@@ -407,6 +427,11 @@ private fun PortraitContent(
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = onShowGamePickerChanged,
|
||||
players = players,
|
||||
controllerMapping = controllerMapping,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiimotePortChanged,
|
||||
showControllerMapping = showControllerMapping,
|
||||
onShowControllerMappingChanged = onShowControllerMappingChanged,
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
networkMode = networkMode,
|
||||
onNetworkModeChanged = onNetworkModeChanged,
|
||||
@@ -441,6 +466,11 @@ private fun LandscapeContent(
|
||||
showGamePicker: Boolean,
|
||||
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||
players: List<Player>,
|
||||
controllerMapping: ControllerMapping,
|
||||
onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
showControllerMapping: Boolean,
|
||||
onShowControllerMappingChanged: (Boolean) -> Unit,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
networkMode: NetworkMode,
|
||||
onNetworkModeChanged: (NetworkMode) -> Unit,
|
||||
@@ -473,7 +503,6 @@ private fun LandscapeContent(
|
||||
.fillMaxHeight()
|
||||
)
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
@@ -492,6 +521,11 @@ private fun LandscapeContent(
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = onShowGamePickerChanged,
|
||||
players = players,
|
||||
controllerMapping = controllerMapping,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiimotePortChanged,
|
||||
showControllerMapping = showControllerMapping,
|
||||
onShowControllerMappingChanged = onShowControllerMappingChanged,
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
networkMode = networkMode,
|
||||
onNetworkModeChanged = onNetworkModeChanged,
|
||||
@@ -513,6 +547,7 @@ private fun LandscapeContent(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PlayersAndSettings(
|
||||
game: String,
|
||||
@@ -521,6 +556,11 @@ private fun PlayersAndSettings(
|
||||
showGamePicker: Boolean,
|
||||
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||
players: List<Player>,
|
||||
controllerMapping: ControllerMapping,
|
||||
onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
showControllerMapping: Boolean,
|
||||
onShowControllerMappingChanged: (Boolean) -> Unit,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
networkMode: NetworkMode,
|
||||
onNetworkModeChanged: (NetworkMode) -> Unit,
|
||||
@@ -558,25 +598,15 @@ private fun PlayersAndSettings(
|
||||
|
||||
MenuSpacer()
|
||||
|
||||
OutlinedBox(
|
||||
label = { Text(stringResource(R.string.netplay_players_label)) },
|
||||
) {
|
||||
PlayersTable(
|
||||
rows = buildList {
|
||||
add(
|
||||
listOf(
|
||||
stringResource(R.string.netplay_players_name),
|
||||
stringResource(R.string.netplay_players_ping),
|
||||
stringResource(R.string.netplay_players_mapping),
|
||||
)
|
||||
)
|
||||
addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) })
|
||||
repeat(4 - players.size) { add(listOf("", "", "")) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Players(
|
||||
players = players,
|
||||
isHosting = isHosting,
|
||||
controllerMapping = controllerMapping,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiimotePortChanged,
|
||||
showControllerMapping = showControllerMapping,
|
||||
onShowControllerMappingChanged = onShowControllerMappingChanged,
|
||||
)
|
||||
|
||||
if (isHosting) {
|
||||
MenuSpacer()
|
||||
@@ -960,6 +990,64 @@ private fun AddressRow(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun Players(
|
||||
players: List<Player>,
|
||||
isHosting: Boolean,
|
||||
controllerMapping: ControllerMapping,
|
||||
onGamecubePortChanged: (Int, Player?) -> Unit,
|
||||
onWiimotePortChanged: (Int, Player?) -> Unit,
|
||||
showControllerMapping: Boolean,
|
||||
onShowControllerMappingChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
val sheetState = rememberSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
initialValue = if (showControllerMapping) SheetValue.Expanded else SheetValue.Hidden,
|
||||
)
|
||||
|
||||
if (showControllerMapping) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { onShowControllerMappingChanged(false) },
|
||||
sheetState = sheetState,
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
ControllerMapping(
|
||||
mapping = controllerMapping,
|
||||
players = players,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiimotePortChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedBox(
|
||||
label = { Text(stringResource(R.string.netplay_players_label)) },
|
||||
onClick = if (isHosting) {
|
||||
{ onShowControllerMappingChanged(true) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
) {
|
||||
PlayersTable(
|
||||
rows = buildList {
|
||||
add(
|
||||
listOf(
|
||||
stringResource(R.string.netplay_players_name),
|
||||
stringResource(R.string.netplay_players_ping),
|
||||
stringResource(R.string.netplay_players_mapping),
|
||||
)
|
||||
)
|
||||
addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) })
|
||||
repeat(4 - players.size) { add(listOf("", "", "")) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A table arranged into columns sized to wrap the largest item. Except the
|
||||
* first column which takes up the remaining space left by the other columns.
|
||||
@@ -972,7 +1060,6 @@ private fun PlayersTable(
|
||||
) {
|
||||
rows.zipWithNext { a, b -> if (a.size != b.size) throw IllegalArgumentException("Rows must all contain the same number of elements.") }
|
||||
val maxWidths = remember { List(rows.first().size) { mutableIntStateOf(0) } }
|
||||
val density = LocalDensity.current
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
@@ -984,23 +1071,21 @@ private fun PlayersTable(
|
||||
) {
|
||||
row.forEachIndexed { itemIndex, text ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
when {
|
||||
itemIndex == 0 -> Modifier.weight(1f)
|
||||
|
||||
maxWidths[itemIndex].intValue > 0 -> Modifier
|
||||
.width(with(density) { maxWidths[itemIndex].intValue.toDp() })
|
||||
|
||||
else -> Modifier
|
||||
modifier = if (itemIndex == 0) {
|
||||
Modifier.weight(1f)
|
||||
} else {
|
||||
val maxWidth = maxWidths[itemIndex]
|
||||
Modifier.layout { measurable, constraints ->
|
||||
val placeable =
|
||||
measurable.measure(constraints.copy(maxWidth = Constraints.Infinity))
|
||||
if (placeable.width > maxWidth.intValue) {
|
||||
maxWidth.intValue = placeable.width
|
||||
}
|
||||
)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
val width = coordinates.size.width
|
||||
if (width > maxWidths[itemIndex].intValue) {
|
||||
maxWidths[itemIndex].intValue = width
|
||||
layout(maxWidth.intValue, placeable.height) {
|
||||
placeable.place(x = 0, y = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
@@ -1017,6 +1102,150 @@ private fun PlayersTable(
|
||||
}
|
||||
}
|
||||
|
||||
private val PORT_DROPDOWN_MIN_WIDTH = 140.dp
|
||||
private val PORT_DROPDOWN_SPACING = 10.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ControllerMapping(
|
||||
mapping: ControllerMapping,
|
||||
players: List<Player>,
|
||||
onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = DolphinTheme.scaffoldPadding)
|
||||
.padding(bottom = 24.dp)
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
val columns =
|
||||
((maxWidth + PORT_DROPDOWN_SPACING) / (PORT_DROPDOWN_MIN_WIDTH + PORT_DROPDOWN_SPACING))
|
||||
.toInt()
|
||||
.coerceAtLeast(1)
|
||||
.let { if (it == 3) 2 else it } // Three per row would look strange so bump down to two.
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
PortsSection(
|
||||
titleId = R.string.netplay_controllers_gamecube,
|
||||
portLabelId = R.string.netplay_controllers_port,
|
||||
mapping = mapping.gamecubePorts,
|
||||
players = players,
|
||||
onPortChanged = onGamecubePortChanged,
|
||||
columns = columns,
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
PortsSection(
|
||||
titleId = R.string.netplay_controllers_wii_remotes,
|
||||
portLabelId = R.string.netplay_controllers_remote,
|
||||
mapping = mapping.wiiRemotes,
|
||||
players = players,
|
||||
onPortChanged = onWiimotePortChanged,
|
||||
columns = columns,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PortsSection(
|
||||
@StringRes titleId: Int,
|
||||
@StringRes portLabelId: Int,
|
||||
mapping: List<Player?>,
|
||||
players: List<Player>,
|
||||
onPortChanged: (port: Int, player: Player?) -> Unit,
|
||||
columns: Int,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(PORT_DROPDOWN_SPACING),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(titleId),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
mapping.indices.chunked(columns).forEach { row ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(PORT_DROPDOWN_SPACING),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
row.forEach { port ->
|
||||
PortDropdown(
|
||||
label = stringResource(portLabelId, port + 1),
|
||||
selectedPlayer = mapping[port],
|
||||
players = players,
|
||||
onPlayerSelected = { onPortChanged(port, it) },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PortDropdown(
|
||||
label: String,
|
||||
selectedPlayer: Player?,
|
||||
players: List<Player>,
|
||||
onPlayerSelected: (player: Player?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
fun Player.displayName() = "$name ($pid)"
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
modifier = modifier,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedPlayer?.displayName()
|
||||
?: stringResource(R.string.netplay_controllers_none),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = true,
|
||||
label = { Text(label) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||
modifier = Modifier
|
||||
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.netplay_controllers_none)) },
|
||||
onClick = {
|
||||
onPlayerSelected(null)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
players.forEach { player ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(player.displayName()) },
|
||||
onClick = {
|
||||
onPlayerSelected(player)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun NetworkModeDropdown(
|
||||
@@ -1473,6 +1702,9 @@ private fun PreviewNetplayScreen() {
|
||||
JoinInfoType.EXTERNAL to JoinAddress.Loaded("203.0.113.1:2626"),
|
||||
JoinInfoType.LOCAL to JoinAddress.Loaded("192.168.1.5:2626"),
|
||||
),
|
||||
controllerMapping = ControllerMapping.emptyControllerMapping(),
|
||||
onGamecubePortChanged = { _, _ -> },
|
||||
onWiiRemoteChanged = { _, _ -> },
|
||||
// saveTransferProgress = SaveTransferProgress(
|
||||
// title = "Title",
|
||||
// totalSize = 1024L,
|
||||
|
||||
@@ -253,7 +253,7 @@ fun OutlinedBox(
|
||||
if (onClick != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.matchParentSize()
|
||||
.clip(MaterialTheme.shapes.extraSmall)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
|
||||
@@ -1031,6 +1031,11 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||
<string name="netplay_players_name">Name</string>
|
||||
<string name="netplay_players_ping">Ping</string>
|
||||
<string name="netplay_players_mapping">Mapping</string>
|
||||
<string name="netplay_controllers_gamecube">GameCube</string>
|
||||
<string name="netplay_controllers_wii_remotes">Wii Remotes</string>
|
||||
<string name="netplay_controllers_port">Port %1$d</string>
|
||||
<string name="netplay_controllers_remote">Remote %1$d</string>
|
||||
<string name="netplay_controllers_none">None</string>
|
||||
<string name="netplay_network_mode_label">Input mode</string>
|
||||
<string name="netplay_network_mode_fair_input_delay">Fair Input Delay</string>
|
||||
<string name="netplay_network_mode_host_input_authority">Host Input Authority</string>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include <jni.h>
|
||||
@@ -38,6 +41,30 @@ static NetPlay::NetPlayServer* GetServerPointer(JNIEnv* env, jobject obj)
|
||||
env->GetLongField(obj, IDCache::GetNetPlayServerPointer()));
|
||||
}
|
||||
|
||||
static jintArray PadMappingArrayToJIntArray(JNIEnv* env, const NetPlay::PadMappingArray& mapping)
|
||||
{
|
||||
const jsize size = static_cast<jsize>(mapping.size());
|
||||
jintArray jmapping = env->NewIntArray(size);
|
||||
std::array<jint, std::tuple_size_v<std::decay_t<decltype(mapping)>>> values;
|
||||
for (size_t i = 0; i < mapping.size(); i++)
|
||||
values[i] = static_cast<jint>(mapping[i]);
|
||||
env->SetIntArrayRegion(jmapping, 0, size, values.data());
|
||||
return jmapping;
|
||||
}
|
||||
|
||||
static bool JIntArrayToPadMappingArray(JNIEnv* env, jintArray jmapping,
|
||||
NetPlay::PadMappingArray* mapping)
|
||||
{
|
||||
if (env->GetArrayLength(jmapping) != static_cast<jsize>(mapping->size()))
|
||||
return false;
|
||||
|
||||
std::array<jint, std::tuple_size_v<std::decay_t<decltype(*mapping)>>> values;
|
||||
env->GetIntArrayRegion(jmapping, 0, static_cast<jsize>(values.size()), values.data());
|
||||
for (size_t i = 0; i < mapping->size(); i++)
|
||||
(*mapping)[i] = static_cast<NetPlay::PlayerId>(values[i]);
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
@@ -224,6 +251,54 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReconnectTr
|
||||
Common::g_TraversalClient->ReconnectToServer();
|
||||
}
|
||||
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetPadMapping(JNIEnv* env,
|
||||
jobject obj)
|
||||
{
|
||||
auto* server = GetServerPointer(env, obj);
|
||||
if (!server)
|
||||
return env->NewIntArray(0);
|
||||
|
||||
return PadMappingArrayToJIntArray(env, server->GetPadMapping());
|
||||
}
|
||||
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetWiimoteMapping(JNIEnv* env,
|
||||
jobject obj)
|
||||
{
|
||||
auto* server = GetServerPointer(env, obj);
|
||||
if (!server)
|
||||
return env->NewIntArray(0);
|
||||
|
||||
return PadMappingArrayToJIntArray(env, server->GetWiimoteMapping());
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSetPadMapping(
|
||||
JNIEnv* env, jobject obj, jintArray jmapping)
|
||||
{
|
||||
auto* server = GetServerPointer(env, obj);
|
||||
if (!server)
|
||||
return;
|
||||
|
||||
NetPlay::PadMappingArray mapping = server->GetPadMapping();
|
||||
if (JIntArrayToPadMappingArray(env, jmapping, &mapping))
|
||||
server->SetPadMapping(mapping);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSetWiimoteMapping(
|
||||
JNIEnv* env, jobject obj, jintArray jmapping)
|
||||
{
|
||||
auto* server = GetServerPointer(env, obj);
|
||||
if (!server)
|
||||
return;
|
||||
|
||||
NetPlay::PadMappingArray mapping = server->GetWiimoteMapping();
|
||||
if (JIntArrayToPadMappingArray(env, jmapping, &mapping))
|
||||
server->SetWiimoteMapping(mapping);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(
|
||||
JNIEnv*, jobject, jlong pointer)
|
||||
|
||||
Reference in New Issue
Block a user