mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-09-28 05:06:57 -05:00
Handle traversal state changes and errors
Traversal connections show in the joining info UI. Non fatal errors show the retry button. Fatal errors end the netplay session.
This commit is contained in:
@@ -25,11 +25,12 @@ import kotlinx.coroutines.flow.runningFold
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
|
||||
import org.dolphinemu.dolphinemu.model.GameFile
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.TraversalState
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
|
||||
import org.dolphinemu.dolphinemu.model.GameFile
|
||||
|
||||
class NetplaySession(
|
||||
private val onClosed: (NetplaySession) -> Unit,
|
||||
@@ -117,6 +118,15 @@ class NetplaySession(
|
||||
private val _gameDigestProgress = MutableStateFlow<GameDigestProgress?>(null)
|
||||
val gameDigestProgress = _gameDigestProgress.asStateFlow()
|
||||
|
||||
private val _traversalState = MutableSharedFlow<TraversalState>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val traversalState = _traversalState.asSharedFlow()
|
||||
|
||||
private val _fatalTraversalError = Channel<TraversalState.Failure>(Channel.CONFLATED)
|
||||
val fatalTraversalError = _fatalTraversalError.receiveAsFlow()
|
||||
|
||||
suspend fun join(): Boolean = withContext(Dispatchers.IO) {
|
||||
if (isClosed) throw IllegalStateException("Cannot join a closed session")
|
||||
|
||||
@@ -148,7 +158,7 @@ class NetplaySession(
|
||||
}
|
||||
|
||||
fun sendMessage(message: String) {
|
||||
_chatMessages.tryEmit( "$nickName: $message")
|
||||
_chatMessages.tryEmit("$nickName: $message")
|
||||
nativeSendMessage(message)
|
||||
}
|
||||
|
||||
@@ -162,6 +172,8 @@ class NetplaySession(
|
||||
|
||||
fun getExternalIpAddress(): String? = nativeGetExternalIpAddress()
|
||||
|
||||
fun reconnectTraversal() = nativeReconnectTraversal()
|
||||
|
||||
fun consumeBootSessionData(): Long {
|
||||
return bootSessionDataPointer.also {
|
||||
bootSessionDataPointer = 0
|
||||
@@ -247,6 +259,8 @@ class NetplaySession(
|
||||
|
||||
private external fun nativeGetExternalIpAddress(): String?
|
||||
|
||||
private external fun nativeReconnectTraversal()
|
||||
|
||||
// NetPlayUI callbacks
|
||||
|
||||
@Keep
|
||||
@@ -390,6 +404,26 @@ class NetplaySession(
|
||||
fun onAbortGameDigest() {
|
||||
_gameDigestProgress.value = null
|
||||
}
|
||||
|
||||
@Keep
|
||||
fun onTraversalStateChanged(
|
||||
state: Int,
|
||||
hostCode: String?,
|
||||
externalAddress: String?,
|
||||
failureReason: String?,
|
||||
) {
|
||||
val traversalState = when (state) {
|
||||
0 -> TraversalState.Connecting
|
||||
1 -> TraversalState.Connected(hostCode!!, externalAddress!!)
|
||||
2 -> TraversalState.Failure(failureReason!!)
|
||||
else -> return
|
||||
}
|
||||
_traversalState.tryEmit(traversalState)
|
||||
|
||||
if (failureReason == "BadHost" || failureReason == "VersionTooOld") {
|
||||
_fatalTraversalError.trySend(TraversalState.Failure(failureReason))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Channel<T>.flush() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.annotation.StringRes
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
|
||||
enum class JoinInfoType(@StringRes val labelId: Int) {
|
||||
ROOM_ID(R.string.netplay_address_type_room_id),
|
||||
EXTERNAL(R.string.netplay_address_type_external),
|
||||
LOCAL(R.string.netplay_address_type_local),
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.dolphinemu.dolphinemu.features.netplay.NetplaySession
|
||||
@@ -28,20 +30,29 @@ class NetplayViewModel(
|
||||
private val networkHelper: NetworkHelper,
|
||||
) : ViewModel() {
|
||||
|
||||
private val isTraversal = StringSetting.NETPLAY_TRAVERSAL_CHOICE.string == "traversal"
|
||||
|
||||
val launchGame = netplaySession.launchGame
|
||||
|
||||
val isHosting = netplaySession.isHosting
|
||||
|
||||
private val _joinAddresses = MutableStateFlow(
|
||||
mapOf(
|
||||
JoinInfoType.EXTERNAL to JoinAddress.Loading,
|
||||
JoinInfoType.LOCAL to getLocalIp(),
|
||||
)
|
||||
buildMap {
|
||||
if (isHosting) {
|
||||
if (isTraversal) {
|
||||
put(JoinInfoType.ROOM_ID, JoinAddress.Loading)
|
||||
}
|
||||
put(JoinInfoType.EXTERNAL, JoinAddress.Loading)
|
||||
put(JoinInfoType.LOCAL, getLocalIp())
|
||||
}
|
||||
}
|
||||
)
|
||||
val joinAddresses = _joinAddresses.asStateFlow()
|
||||
|
||||
val connectionLost = netplaySession.connectionLost
|
||||
|
||||
val fatalTraversalError = netplaySession.fatalTraversalError
|
||||
|
||||
val players = netplaySession.players
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
@@ -72,7 +83,11 @@ class NetplayViewModel(
|
||||
init {
|
||||
if (netplaySession.isHosting) {
|
||||
setInitialGame()
|
||||
fetchExternalIp()
|
||||
if (isTraversal) {
|
||||
collectTraversalState()
|
||||
} else {
|
||||
fetchExternalIp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +133,32 @@ class NetplayViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectTraversalState() {
|
||||
val retry = { netplaySession.reconnectTraversal() }
|
||||
netplaySession.traversalState.onEach { state ->
|
||||
when (state) {
|
||||
is TraversalState.Connecting -> {
|
||||
_joinAddresses.value += mapOf(
|
||||
JoinInfoType.ROOM_ID to JoinAddress.Loading,
|
||||
JoinInfoType.EXTERNAL to JoinAddress.Loading,
|
||||
)
|
||||
}
|
||||
is TraversalState.Connected -> {
|
||||
_joinAddresses.value += mapOf(
|
||||
JoinInfoType.ROOM_ID to JoinAddress.Loaded(state.hostCode),
|
||||
JoinInfoType.EXTERNAL to JoinAddress.Loaded(state.externalAddress),
|
||||
)
|
||||
}
|
||||
is TraversalState.Failure -> {
|
||||
_joinAddresses.value += mapOf(
|
||||
JoinInfoType.ROOM_ID to JoinAddress.Unknown(retry),
|
||||
JoinInfoType.EXTERNAL to JoinAddress.Unknown(retry),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun setInitialGame() {
|
||||
val game = gameFiles.value
|
||||
.find { it.getGameId() == StringSetting.NETPLAY_GAME.string }
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.dolphinemu.dolphinemu.features.netplay.model
|
||||
|
||||
import android.content.Context
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
|
||||
sealed class TraversalState {
|
||||
data object Connecting : TraversalState()
|
||||
|
||||
data class Connected(val hostCode: String, val externalAddress: String) : TraversalState()
|
||||
|
||||
data class Failure(val reason: String) : TraversalState() {
|
||||
fun message(context: Context) = when (reason) {
|
||||
"BadHost" -> context.getString(R.string.netplay_traversal_error_bad_host)
|
||||
"VersionTooOld" -> context.getString(R.string.netplay_traversal_error_version_too_old)
|
||||
else -> reason
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
|
||||
NetplayScreen(
|
||||
onBackClicked = { finish() },
|
||||
connectionLost = viewModel.connectionLost,
|
||||
fatalTraversalError = viewModel.fatalTraversalError,
|
||||
messages = viewModel.messages.collectAsState().value,
|
||||
onSendMessage = viewModel::sendMessage,
|
||||
game = viewModel.game.collectAsState().value,
|
||||
|
||||
@@ -97,6 +97,7 @@ import org.dolphinemu.dolphinemu.features.netplay.model.JoinInfoType
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.TraversalState
|
||||
import org.dolphinemu.dolphinemu.model.GameFile
|
||||
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
|
||||
import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
|
||||
@@ -112,6 +113,7 @@ fun NetplayScreen(
|
||||
onBackClicked: () -> Unit,
|
||||
isHosting: Boolean,
|
||||
connectionLost: Flow<Unit>,
|
||||
fatalTraversalError: Flow<TraversalState.Failure>,
|
||||
messages: List<NetplayMessage>,
|
||||
onSendMessage: (String) -> Unit,
|
||||
game: String,
|
||||
@@ -156,7 +158,9 @@ fun NetplayScreen(
|
||||
// State which must live above the landscape/portrait split.
|
||||
var showChat by rememberSaveable { mutableStateOf(false) }
|
||||
var showGamePicker by rememberSaveable { mutableStateOf(false) }
|
||||
var selectedJoinInfoType by rememberSaveable { mutableStateOf(JoinInfoType.EXTERNAL) }
|
||||
var selectedJoinInfoType by rememberSaveable {
|
||||
mutableStateOf(joinAddresses.keys.firstOrNull() ?: JoinInfoType.EXTERNAL)
|
||||
}
|
||||
|
||||
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
LandscapeContent(
|
||||
@@ -207,6 +211,11 @@ fun NetplayScreen(
|
||||
connectionLost.collect { showConnectionLostDialog = true }
|
||||
}
|
||||
|
||||
var traversalError by rememberSaveable { mutableStateOf<TraversalState.Failure?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
fatalTraversalError.collect { traversalError = it }
|
||||
}
|
||||
|
||||
var dismissSaveTransferProgressDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (saveTransferProgress == null) {
|
||||
dismissSaveTransferProgressDialog = false
|
||||
@@ -217,6 +226,8 @@ fun NetplayScreen(
|
||||
dismissGameDigestDialog = false
|
||||
}
|
||||
|
||||
val currentTraversalError = traversalError
|
||||
|
||||
when {
|
||||
showConnectionLostDialog -> {
|
||||
AlertDialog(
|
||||
@@ -230,6 +241,18 @@ fun NetplayScreen(
|
||||
)
|
||||
}
|
||||
|
||||
currentTraversalError != null -> {
|
||||
AlertDialog(
|
||||
text = { Text(currentTraversalError.message(LocalContext.current)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onBackClicked) {
|
||||
Text(stringResource(R.string.ok))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onBackClicked,
|
||||
)
|
||||
}
|
||||
|
||||
saveTransferProgress != null && !dismissSaveTransferProgressDialog -> {
|
||||
SaveTransferProgressDialog(
|
||||
saveTransferProgress = saveTransferProgress,
|
||||
@@ -669,6 +692,7 @@ private fun JoinAddressSection(
|
||||
modifier = Modifier.weight(0.39f),
|
||||
)
|
||||
AddressRow(
|
||||
joinInfoType = selectedType,
|
||||
address = address,
|
||||
modifier = Modifier.weight(0.61f),
|
||||
)
|
||||
@@ -683,6 +707,7 @@ private fun JoinAddressSection(
|
||||
)
|
||||
MenuSpacer()
|
||||
AddressRow(
|
||||
joinInfoType = selectedType,
|
||||
address = address,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
@@ -737,6 +762,7 @@ private fun JoinInfoDropdown(
|
||||
|
||||
@Composable
|
||||
private fun AddressRow(
|
||||
joinInfoType: JoinInfoType,
|
||||
address: JoinAddress,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -748,7 +774,10 @@ private fun AddressRow(
|
||||
is JoinAddress.Loaded -> address.address
|
||||
is JoinAddress.Unknown -> stringResource(R.string.netplay_address_unknown)
|
||||
},
|
||||
label = stringResource(R.string.netplay_address_label),
|
||||
label = stringResource(
|
||||
if (joinInfoType == JoinInfoType.ROOM_ID) R.string.netplay_code_label
|
||||
else R.string.netplay_address_label
|
||||
),
|
||||
onClick = when (address) {
|
||||
is JoinAddress.Loaded -> {
|
||||
{
|
||||
@@ -1128,6 +1157,7 @@ private fun PreviewNetplayScreen() {
|
||||
NetplayScreen(
|
||||
onBackClicked = {},
|
||||
connectionLost = emptyFlow(),
|
||||
fatalTraversalError = emptyFlow(),
|
||||
players = listOf(
|
||||
Player(
|
||||
pid = 1,
|
||||
|
||||
@@ -1018,8 +1018,12 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||
<string name="netplay_use_upnp">Forward port (UPnP)</string>
|
||||
<string name="netplay_host_address_label">Join info</string>
|
||||
<string name="netplay_address_label">Address</string>
|
||||
<string name="netplay_address_type_external">External IP</string>
|
||||
<string name="netplay_address_type_local">Local IP</string>
|
||||
<string name="netplay_code_label">Code</string>
|
||||
<string name="netplay_address_type_room_id">Room</string>
|
||||
<string name="netplay_address_type_external">External</string>
|
||||
<string name="netplay_address_type_local">Local</string>
|
||||
<string name="netplay_traversal_error_bad_host">Couldn\'t look up central server</string>
|
||||
<string name="netplay_traversal_error_version_too_old">Dolphin is too old for traversal server</string>
|
||||
<string name="netplay_address_loading">Loading…</string>
|
||||
<string name="netplay_address_unknown">Unknown</string>
|
||||
<string name="netplay_address_share">Share address</string>
|
||||
|
||||
Reference in New Issue
Block a user