Merge pull request #14750 from tom-pratt/netplay-controller-mapping

Android: Netplay controller mapping
This commit is contained in:
JosJuice
2026-08-29 11:19:47 +02:00
committed by GitHub
8 changed files with 424 additions and 40 deletions
@@ -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
@@ -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 }
@@ -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() }
@@ -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,
)
}
}
@@ -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>
+75
View File
@@ -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)