diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index 56676c4d3f..d44e70ba09 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -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( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val controllerMapping = _controllerMapping.asSharedFlow().distinctUntilChanged() + private val _chatMessages = MutableSharedFlow( 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) { - _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 diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ControllerMapping.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ControllerMapping.kt new file mode 100644 index 0000000000..d7cfae0ab3 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/ControllerMapping.kt @@ -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, + val wiiRemotes: List, +) { + 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 List.replace(index: Int, value: T) = + toMutableList().also { it[index] = value } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt index 0764448824..1e3aea26a2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt @@ -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() } diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index d6e24d250b..287f2cdd27 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -1,6 +1,7 @@ // Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -38,6 +39,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(mapping.size()); + jintArray jmapping = env->NewIntArray(size); + std::array values; + for (size_t i = 0; i < mapping.size(); i++) + values[i] = static_cast(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(mapping->size())) + return false; + + std::array values; + env->GetIntArrayRegion(jmapping, 0, static_cast(values.size()), values.data()); + for (size_t i = 0; i < mapping->size(); i++) + (*mapping)[i] = static_cast(values[i]); + return true; +} + extern "C" { JNIEXPORT void JNICALL @@ -224,6 +249,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)