mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-05-09 04:13:28 -05:00
Show client connection errors and handle connection result
If result is a success sent event to launch the next netplay screen. if it fails, clear up the netplay client
This commit is contained in:
parent
12343ebf86
commit
b2e900ce40
|
|
@ -4,8 +4,11 @@
|
|||
package org.dolphinemu.dolphinemu.features.netplay
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.ConnectionType
|
||||
|
||||
object Netplay {
|
||||
|
|
@ -21,6 +24,12 @@ object Netplay {
|
|||
private val _launchGame = Channel<String>(Channel.CONFLATED)
|
||||
val launchGame = _launchGame.receiveAsFlow()
|
||||
|
||||
private val _connectionErrors = Channel<String>(Channel.BUFFERED)
|
||||
val connectionErrors = _connectionErrors.receiveAsFlow()
|
||||
|
||||
@JvmStatic
|
||||
external fun isClientConnected(): Boolean
|
||||
|
||||
@JvmStatic
|
||||
external fun getNickname(): String
|
||||
|
||||
|
|
@ -63,13 +72,13 @@ object Netplay {
|
|||
@JvmStatic
|
||||
external fun getIndexPassword(): String
|
||||
|
||||
fun saveSetup(
|
||||
suspend fun saveSetup(
|
||||
nickname: String,
|
||||
connectionType: ConnectionType,
|
||||
address: String,
|
||||
hostCode: String,
|
||||
connectPort: Int,
|
||||
) {
|
||||
) = withContext(Dispatchers.IO) {
|
||||
SaveSetup(
|
||||
nickname = nickname,
|
||||
traversalChoice = connectionType.configValue,
|
||||
|
|
@ -108,16 +117,50 @@ object Netplay {
|
|||
indexPassword: String,
|
||||
)
|
||||
|
||||
fun join() {
|
||||
suspend fun join(): Boolean = withContext(Dispatchers.IO) {
|
||||
netPlayClientPointer = Join()
|
||||
val isConnected = netPlayClientPointer != 0L && isClientConnected()
|
||||
|
||||
if (!isActive) {
|
||||
releaseNetplayClient()
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
if (isConnected) {
|
||||
return@withContext true
|
||||
}
|
||||
|
||||
releaseNetplayClient()
|
||||
false
|
||||
}
|
||||
|
||||
private fun releaseNetplayClient() {
|
||||
if (netPlayClientPointer != 0L) {
|
||||
ReleaseNetplayClient()
|
||||
netPlayClientPointer = 0
|
||||
}
|
||||
_launchGame.flush()
|
||||
_connectionErrors.flush()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private external fun Join(): Long
|
||||
|
||||
@JvmStatic
|
||||
private external fun ReleaseNetplayClient()
|
||||
|
||||
@JvmStatic
|
||||
fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) {
|
||||
this.bootSessionDataPointer = bootSessionDataPointer
|
||||
_launchGame.trySend(gameFilePath)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun onConnectionError(message: String) {
|
||||
_connectionErrors.trySend(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Channel<String>.flush() {
|
||||
while (this.tryReceive().isSuccess) Unit
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,17 @@ import org.dolphinemu.dolphinemu.R
|
|||
|
||||
sealed class ConnectionRole(
|
||||
@StringRes val labelId: Int,
|
||||
@StringRes val loadingLabelId: Int,
|
||||
) {
|
||||
object Connect : ConnectionRole(R.string.netplay_connection_role_connect)
|
||||
object Host : ConnectionRole(R.string.netplay_connection_role_host)
|
||||
object Connect : ConnectionRole(
|
||||
labelId = R.string.netplay_connection_role_connect,
|
||||
loadingLabelId = R.string.netplay_connection_role_connect_loading,
|
||||
)
|
||||
|
||||
object Host : ConnectionRole(
|
||||
labelId = R.string.netplay_connection_role_host,
|
||||
loadingLabelId = R.string.netplay_connection_role_host_loading,
|
||||
)
|
||||
|
||||
companion object {
|
||||
val all: List<ConnectionRole>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,15 @@
|
|||
package org.dolphinemu.dolphinemu.features.netplay.model
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.dolphinemu.dolphinemu.features.netplay.Netplay
|
||||
import org.dolphinemu.dolphinemu.services.GameFileCacheManager
|
||||
|
||||
|
|
@ -27,6 +34,14 @@ class NetplaySetupViewModel : ViewModel() {
|
|||
private val _connectPort = MutableStateFlow(Netplay.getConnectPort().toString())
|
||||
val connectPort = _connectPort.asStateFlow()
|
||||
|
||||
private val _showNetplayScreen = Channel<Unit>(CONFLATED)
|
||||
val showNetplayScreen = _showNetplayScreen.receiveAsFlow()
|
||||
|
||||
private val _connecting = MutableStateFlow(false)
|
||||
val connecting = _connecting.asStateFlow()
|
||||
|
||||
val errors = Netplay.connectionErrors
|
||||
|
||||
init {
|
||||
GameFileCacheManager.startLoad()
|
||||
}
|
||||
|
|
@ -60,17 +75,24 @@ class NetplaySetupViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
fun connect() {
|
||||
if (GameFileCacheManager.isLoading().value == true) {
|
||||
return
|
||||
}
|
||||
_connecting.value = true
|
||||
|
||||
Netplay.saveSetup(
|
||||
nickname = nickname.value,
|
||||
connectionType = connectionType.value,
|
||||
address = ipAddress.value,
|
||||
hostCode = hostCode.value,
|
||||
connectPort = connectPort.value.toInt(),
|
||||
)
|
||||
Netplay.join()
|
||||
viewModelScope.launch {
|
||||
GameFileCacheManager.isLoading().asFlow().first { it == false }
|
||||
|
||||
Netplay.saveSetup(
|
||||
nickname = nickname.value,
|
||||
connectionType = connectionType.value,
|
||||
address = ipAddress.value,
|
||||
hostCode = hostCode.value,
|
||||
connectPort = connectPort.value.toInt(),
|
||||
)
|
||||
|
||||
if (Netplay.join()) {
|
||||
_showNetplayScreen.trySend(Unit)
|
||||
}
|
||||
|
||||
_connecting.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,19 +11,24 @@ import androidx.activity.compose.setContent
|
|||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
|
|
@ -35,7 +40,9 @@ import androidx.compose.material3.Scaffold
|
|||
import androidx.compose.material3.SecondaryTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -45,8 +52,12 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
|
|
@ -60,6 +71,10 @@ import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
|
|||
import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
|
||||
import org.dolphinemu.dolphinemu.utils.ThemeHelper
|
||||
|
||||
private data class ErrorDialogState(val message: String) {
|
||||
val onDismissed = CompletableDeferred<Unit>()
|
||||
}
|
||||
|
||||
class NetplaySetupActivity : AppCompatActivity(), ThemeProvider {
|
||||
override var themeId: Int = 0
|
||||
private lateinit var viewModel: NetplaySetupViewModel
|
||||
|
|
@ -75,10 +90,16 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider {
|
|||
|
||||
viewModel = ViewModelProvider(this)[NetplaySetupViewModel::class.java]
|
||||
|
||||
viewModel.showNetplayScreen
|
||||
.onEach { /* launch NetplayActivity */ }
|
||||
.launchIn(lifecycleScope)
|
||||
|
||||
setContent {
|
||||
DolphinTheme {
|
||||
NetplaySetupScreen(
|
||||
onBackClicked = { finish() },
|
||||
connecting = viewModel.connecting.collectAsState().value,
|
||||
errors = viewModel.errors,
|
||||
nickname = viewModel.nickname.collectAsState().value,
|
||||
onNicknameChanged = viewModel::setNickname,
|
||||
connectionType = viewModel.connectionType.collectAsState().value,
|
||||
|
|
@ -118,6 +139,8 @@ class NetplaySetupActivity : AppCompatActivity(), ThemeProvider {
|
|||
@Composable
|
||||
private fun NetplaySetupScreen(
|
||||
onBackClicked: () -> Unit,
|
||||
connecting: Boolean,
|
||||
errors: Flow<String>,
|
||||
connectionRole: ConnectionRole,
|
||||
onConnectionRoleChanged: (ConnectionRole) -> Unit,
|
||||
nickname: String,
|
||||
|
|
@ -150,10 +173,40 @@ private fun NetplaySetupScreen(
|
|||
ExtendedFloatingActionButton(
|
||||
onClick = onConnectClicked,
|
||||
) {
|
||||
Text(stringResource(connectionRole.labelId))
|
||||
if (connecting) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(stringResource(connectionRole.loadingLabelId))
|
||||
} else {
|
||||
Text(stringResource(connectionRole.labelId))
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
var activeErrorDialog by remember { mutableStateOf<ErrorDialogState?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
errors.collect { message ->
|
||||
activeErrorDialog = ErrorDialogState(message)
|
||||
activeErrorDialog?.onDismissed?.await()
|
||||
activeErrorDialog = null
|
||||
}
|
||||
}
|
||||
activeErrorDialog?.let { activeErrorDialog ->
|
||||
AlertDialog(
|
||||
text = { Text(activeErrorDialog.message) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { activeErrorDialog.onDismissed.complete(Unit) }) {
|
||||
Text("Dismiss")
|
||||
}
|
||||
},
|
||||
onDismissRequest = { activeErrorDialog.onDismissed.complete(Unit) },
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -332,12 +385,14 @@ private fun NetplaySetupScreenPreview() {
|
|||
MaterialTheme {
|
||||
NetplaySetupScreen(
|
||||
onBackClicked = {},
|
||||
connecting = false,
|
||||
errors = emptyFlow(),
|
||||
connectionRole = ConnectionRole.Connect,
|
||||
onConnectionRoleChanged = {},
|
||||
nickname = "Preview nickname",
|
||||
onNicknameChanged = {},
|
||||
connectionType = ConnectionType.DirectConnection,
|
||||
onConnectionTypeChanged = {},
|
||||
connectionRole = ConnectionRole.Connect,
|
||||
onConnectionRoleChanged = {},
|
||||
ipAddress = "127.0.0.1",
|
||||
onIpAddressChanged = {},
|
||||
connectPort = "2626",
|
||||
|
|
|
|||
|
|
@ -984,12 +984,14 @@ It can efficiently compress both junk data and encrypted Wii data.
|
|||
|
||||
<!-- Netplay -->
|
||||
<string name="netplay_setup_title">Netplay Setup</string>
|
||||
<string name="netplay_nickname_label">Nickname</string>
|
||||
<string name="netplay_connection_type">Connection type</string>
|
||||
<string name="netplay_connection_type_direct_connection">Direct connection</string>
|
||||
<string name="netplay_connection_type_traversal_server">Traversal server</string>
|
||||
<string name="netplay_connection_role_connect">Connect</string>
|
||||
<string name="netplay_connection_role_connect_loading">Connecting…</string>
|
||||
<string name="netplay_connection_role_host">Host</string>
|
||||
<string name="netplay_connection_role_host_loading">Starting…</string>
|
||||
<string name="netplay_nickname_label">Nickname</string>
|
||||
<string name="netplay_ip_address_label">IP address</string>
|
||||
<string name="netplay_host_code_label">Host code</string>
|
||||
<string name="netplay_port_label">Port</string>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ static jclass s_netplay_class;
|
|||
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_connection_error;
|
||||
|
||||
static jclass s_analytics_class;
|
||||
static jmethodID s_get_analytics_value;
|
||||
|
|
@ -258,6 +259,11 @@ jmethodID GetNetplayOnBootGame()
|
|||
return s_netplay_on_boot_game;
|
||||
}
|
||||
|
||||
jmethodID GetNetplayOnConnectionError()
|
||||
{
|
||||
return s_netplay_on_connection_error;
|
||||
}
|
||||
|
||||
jclass GetPairClass()
|
||||
{
|
||||
return s_pair_class;
|
||||
|
|
@ -668,6 +674,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
|||
s_net_play_client_pointer = env->GetStaticFieldID(netplay_class, "netPlayClientPointer", "J");
|
||||
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_connection_error = env->GetStaticMethodID(netplay_class, "onConnectionError", "(Ljava/lang/String;)V");
|
||||
env->DeleteLocalRef(netplay_class);
|
||||
|
||||
const jclass analytics_class = env->FindClass("org/dolphinemu/dolphinemu/utils/Analytics");
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ jclass GetNetplayClass();
|
|||
jfieldID GetNetPlayClientPointer();
|
||||
jfieldID GetNetplayBootSessionDataPointer();
|
||||
jmethodID GetNetplayOnBootGame();
|
||||
jmethodID GetNetplayOnConnectionError();
|
||||
|
||||
jclass GetPairClass();
|
||||
jmethodID GetPairConstructor();
|
||||
|
|
|
|||
|
|
@ -56,7 +56,14 @@ void NetPlayUICallbacks::OnPadBufferChanged(u32) {}
|
|||
void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool) {}
|
||||
void NetPlayUICallbacks::OnDesync(u32, const std::string&) {}
|
||||
void NetPlayUICallbacks::OnConnectionLost() {}
|
||||
void NetPlayUICallbacks::OnConnectionError(const std::string&) {}
|
||||
|
||||
void NetPlayUICallbacks::OnConnectionError(const std::string& message)
|
||||
{
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnConnectionError(),
|
||||
ToJString(env, message));
|
||||
}
|
||||
|
||||
void NetPlayUICallbacks::OnTraversalError(Common::TraversalClient::FailureReason) {}
|
||||
void NetPlayUICallbacks::OnTraversalStateChanged(Common::TraversalClient::State) {}
|
||||
void NetPlayUICallbacks::OnGameStartAborted() {}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@
|
|||
#include "jni/AndroidCommon/IDCache.h"
|
||||
#include "jni/NetPlay/NetPlayUICallbacks.h"
|
||||
|
||||
static NetPlay::NetPlayClient* GetPointer(JNIEnv* env)
|
||||
{
|
||||
return reinterpret_cast<NetPlay::NetPlayClient*>(
|
||||
env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer()));
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
|
|
@ -124,6 +130,12 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_SaveSetup(
|
|||
Config::SetBaseOrCurrent(Config::NETPLAY_LISTEN_PORT, static_cast<u16>(listenPort));
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_isClientConnected(JNIEnv* env, jclass)
|
||||
{
|
||||
return static_cast<jboolean>(GetPointer(env)->IsConnected());
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass)
|
||||
{
|
||||
|
|
@ -155,4 +167,10 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass
|
|||
return reinterpret_cast<jlong>(client);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_ReleaseNetplayClient(JNIEnv* env, jclass)
|
||||
{
|
||||
delete GetPointer(env);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user