From ee1271e5b2ea6c2855c1864d9b23988f6a09daa4 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Sat, 25 Apr 2026 17:40:40 +0200 Subject: [PATCH] Implement OnConnectionLost, show a dialog on the main netplay screen forcing the user to exit --- .../dolphinemu/features/netplay/Netplay.kt | 11 ++++++++- .../netplay/model/NetplayViewModel.kt | 2 ++ .../features/netplay/ui/NetplayActivity.kt | 1 + .../features/netplay/ui/NetplayScreen.kt | 24 ++++++++++++++++++- .../app/src/main/res/values/strings.xml | 1 + Source/Android/jni/AndroidCommon/IDCache.cpp | 7 ++++++ Source/Android/jni/AndroidCommon/IDCache.h | 1 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 7 +++++- 8 files changed, 51 insertions(+), 3 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt index a90d1b00d8..7298795d71 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/Netplay.kt @@ -47,6 +47,9 @@ object Netplay { private val _stopGame = Channel(Channel.CONFLATED) val stopGame = _stopGame.receiveAsFlow() + private val _connectionLost = Channel(Channel.CONFLATED) + val connectionLost = _connectionLost.receiveAsFlow() + private val _connectionErrors = Channel(Channel.BUFFERED) val connectionErrors = _connectionErrors.receiveAsFlow() @@ -63,7 +66,7 @@ object Netplay { val players = _players.asSharedFlow().distinctUntilChanged() private val _chatMessages = MutableSharedFlow( - replay = 1, + extraBufferCapacity = 32, onBufferOverflow = BufferOverflow.DROP_OLDEST ) val chatMessages = _chatMessages.asSharedFlow() @@ -180,6 +183,12 @@ object Netplay { _stopGame.trySend(Unit) } + @Keep + @JvmStatic + fun onConnectionLost() { + _connectionLost.trySend(Unit) + } + @JvmStatic fun onConnectionError(message: String) { _connectionErrors.trySend(message) 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 217991f40c..955fb876dd 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 @@ -25,6 +25,8 @@ class NetplayViewModel : ViewModel() { private val _goBack = Channel(CONFLATED) val goBack = _goBack.receiveAsFlow() + val connectionLost = Netplay.connectionLost + val players = Netplay.players .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt index fec0823456..463ad31506 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt @@ -44,6 +44,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { DolphinTheme { NetplayScreen( onBackClicked = { finish() }, + connectionLost = viewModel.connectionLost, messages = viewModel.messages.collectAsState().value, onSendMessage = viewModel::sendMessage, game = viewModel.game.collectAsState().value, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt index e9181151e3..c47c8b8e3e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider @@ -43,6 +44,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -63,6 +65,8 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player @@ -75,6 +79,7 @@ import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme @Composable fun NetplayScreen( onBackClicked: () -> Unit, + connectionLost: Flow, messages: List, onSendMessage: (String) -> Unit, game: String, @@ -126,6 +131,22 @@ fun NetplayScreen( modifier = modifier ) } + + var showConnectionLostDialog by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + connectionLost.collect { showConnectionLostDialog = true } + } + if (showConnectionLostDialog) { + AlertDialog( + text = { Text(stringResource(R.string.netplay_connection_lost)) }, + confirmButton = { + TextButton(onClick = onBackClicked) { + Text(stringResource(R.string.ok)) + } + }, + onDismissRequest = onBackClicked, + ) + } } } @@ -246,7 +267,7 @@ private fun PLayersAndSettings( .fillMaxWidth() ) } - + if (hostInputAuthorityEnabled) { MenuSpacer() @@ -539,6 +560,7 @@ private fun LandscapeNetplayScreenDarkPreview() { private fun PreviewNetplayScreen() { NetplayScreen( onBackClicked = {}, + connectionLost = emptyFlow(), players = listOf( Player( pid = 1, diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 0bd19d654b..7b4311c996 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1008,4 +1008,5 @@ It can efficiently compress both junk data and encrypted Wii data. Ping Mapping Max buffer + Netplay connection lost diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index bc59fe0053..461576f217 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -33,6 +33,7 @@ static jfieldID s_net_play_client_pointer; static jfieldID s_netplay_boot_session_data_pointer; static jmethodID s_netplay_on_boot_game; static jmethodID s_netplay_on_stop_game; +static jmethodID s_netplay_on_connection_lost; static jmethodID s_netplay_on_connection_error; static jmethodID s_netplay_on_game_changed; static jmethodID s_netplay_on_host_input_authority_changed; @@ -273,6 +274,11 @@ jmethodID GetNetplayOnStopGame() return s_netplay_on_stop_game; } +jmethodID GetNetplayOnConnectionLost() +{ + return s_netplay_on_connection_lost; +} + jmethodID GetNetplayOnConnectionError() { return s_netplay_on_connection_error; @@ -724,6 +730,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_netplay_boot_session_data_pointer = env->GetStaticFieldID(netplay_class, "bootSessionDataPointer", "J"); s_netplay_on_boot_game = env->GetStaticMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); s_netplay_on_stop_game = env->GetStaticMethodID(netplay_class, "onStopGame", "()V"); + s_netplay_on_connection_lost = env->GetStaticMethodID(netplay_class, "onConnectionLost", "()V"); s_netplay_on_connection_error = env->GetStaticMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V"); s_netplay_on_game_changed = env->GetStaticMethodID(netplay_class, "onGameChanged", "(Ljava/lang/String;)V"); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 5646669f18..c535fcb9ae 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -36,6 +36,7 @@ jfieldID GetNetPlayClientPointer(); jfieldID GetNetplayBootSessionDataPointer(); jmethodID GetNetplayOnBootGame(); jmethodID GetNetplayOnStopGame(); +jmethodID GetNetplayOnConnectionLost(); jmethodID GetNetplayOnConnectionError(); jmethodID GetNetplayOnGameChanged(); jmethodID GetNetplayOnHostInputAuthorityChanged(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index dd25d54280..21dd77d019 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -135,7 +135,12 @@ void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled) } void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} -void NetPlayUICallbacks::OnConnectionLost() {} + +void NetPlayUICallbacks::OnConnectionLost() +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnConnectionLost()); +} void NetPlayUICallbacks::OnConnectionError(const std::string& message) {