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 adea1a13a5..0764448824 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 @@ -11,15 +11,16 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.receiveAsFlow 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.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.NetplaySession +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting import org.dolphinemu.dolphinemu.features.settings.model.IntSetting import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig import org.dolphinemu.dolphinemu.features.settings.model.StringSetting @@ -104,18 +105,37 @@ class NetplayViewModel( private val _notAllPlayersHaveGame = Channel(Channel.CONFLATED) val notAllPlayersHaveGame = _notAllPlayersHaveGame.receiveAsFlow() + private val _dualCoreWarning = Channel(Channel.CONFLATED) + val dualCoreWarning = _dualCoreWarning.receiveAsFlow() + fun startGame() { - if (netplaySession.doAllPlayersHaveGame()) { - netplaySession.startGame() - } else { + if (!netplaySession.doAllPlayersHaveGame()) { _notAllPlayersHaveGame.trySend(Unit) + return } + + if (BooleanSetting.MAIN_CPU_THREAD.boolean && + !BooleanSetting.NETPLAY_SKIP_DUAL_CORE_WARNING.boolean + ) { + _dualCoreWarning.trySend(Unit) + return + } + + confirmStartGame() } fun confirmStartGame() { netplaySession.startGame() } + fun setDualCoreEnabled(enabled: Boolean) { + BooleanSetting.MAIN_CPU_THREAD.setBoolean(NativeConfig.LAYER_BASE, enabled) + } + + fun skipDualCoreWarning() { + BooleanSetting.NETPLAY_SKIP_DUAL_CORE_WARNING.setBoolean(NativeConfig.LAYER_BASE, true) + } + fun sendMessage(message: String) { val trimmedMessage = message.trim() if (trimmedMessage.isEmpty()) { @@ -176,12 +196,14 @@ class NetplayViewModel( 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), 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 eb9cecec20..28e0e4ca16 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 @@ -58,6 +58,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { onGameSelected = viewModel::changeGame, gameFiles = viewModel.gameFiles.collectAsState().value, notAllPlayersHaveGame = viewModel.notAllPlayersHaveGame, + dualCoreWarning = viewModel.dualCoreWarning, + onSetDualCoreEnabled = viewModel::setDualCoreEnabled, + onSkipDualCoreWarning = viewModel::skipDualCoreWarning, onConfirmStartGame = viewModel::confirmStartGame, players = viewModel.players.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.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 789a3cef17..7df8ea35ee 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 @@ -5,6 +5,7 @@ package org.dolphinemu.dolphinemu.features.netplay.ui import android.content.Intent import android.content.res.Configuration import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -37,6 +38,7 @@ 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 @@ -59,6 +61,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SheetValue +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo @@ -72,7 +75,10 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +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 @@ -87,7 +93,6 @@ 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.dp -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.sp import androidx.window.core.layout.WindowSizeClass import coil.compose.AsyncImage @@ -129,6 +134,9 @@ fun NetplayScreen( onGameSelected: (GameFile) -> Unit, gameFiles: List, notAllPlayersHaveGame: Flow, + dualCoreWarning: Flow, + onSetDualCoreEnabled: (Boolean) -> Unit, + onSkipDualCoreWarning: () -> Unit, onConfirmStartGame: () -> Unit, hostInputAuthorityEnabled: Boolean, networkMode: NetworkMode, @@ -252,6 +260,11 @@ fun NetplayScreen( notAllPlayersHaveGame.collect { showNotAllPlayersHaveGame = true } } + var showDualCoreWarning by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(Unit) { + dualCoreWarning.collect { showDualCoreWarning = true } + } + var dismissSaveTransferProgressDialog by rememberSaveable { mutableStateOf(false) } if (saveTransferProgress == null) { dismissSaveTransferProgressDialog = false @@ -323,6 +336,21 @@ fun NetplayScreen( onDismissRequest = { showNotAllPlayersHaveGame = false }, ) } + + showDualCoreWarning -> { + DualCoreWarningDialog( + onSetDualCoreEnabled = onSetDualCoreEnabled, + onStartGame = { + showDualCoreWarning = false + onConfirmStartGame() + }, + onSkipWarning = { + showDualCoreWarning = false + onSkipDualCoreWarning() + }, + onDismiss = { showDualCoreWarning = false }, + ) + } } } } @@ -1273,6 +1301,73 @@ private fun GameDigestPlayerRow( } } +@Composable +private fun DualCoreWarningDialog( + onSetDualCoreEnabled: (Boolean) -> Unit, + onStartGame: () -> Unit, + onSkipWarning: () -> Unit, + onDismiss: () -> Unit, +) { + var dualCoreEnabled by rememberSaveable { mutableStateOf(true) } + AlertDialog( + title = { Text(stringResource(R.string.netplay_dual_core_warning_title)) }, + text = { + Column { + Text( + text = stringResource(R.string.netplay_dual_core_warning_message), + ) + val ripplePadding = 12.dp + Spacer(Modifier.height(12.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .layout { measurable, constraints -> + // Enlarge the ripple area beyond the dialog's padding + val hPx = ripplePadding.roundToPx() + val placeable = measurable.measure( + constraints.copy( + minWidth = constraints.maxWidth + hPx * 2, + maxWidth = constraints.maxWidth + hPx * 2 + ) + ) + layout(constraints.maxWidth, placeable.height) { + placeable.place(-hPx, 0) + } + } + .clip(MaterialTheme.shapes.small) + .clickable { + dualCoreEnabled = !dualCoreEnabled + onSetDualCoreEnabled(dualCoreEnabled) + } + .padding(ripplePadding), + ) { + Text( + text = stringResource(R.string.dual_core), + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + Switch( + checked = dualCoreEnabled, + onCheckedChange = null, + ) + } + } + }, + confirmButton = { + TextButton(onClick = onStartGame) { + Text(stringResource(R.string.netplay_start)) + } + }, + dismissButton = { + TextButton(onClick = onSkipWarning) { + Text(stringResource(R.string.netplay_dont_warn_again)) + } + }, + onDismissRequest = onDismiss, + ) +} + @Composable private fun NetplayMessage.color(): Color { val isDark = isSystemInDarkTheme() @@ -1361,6 +1456,9 @@ private fun PreviewNetplayScreen() { onGameSelected = {}, gameFiles = emptyList(), notAllPlayersHaveGame = emptyFlow(), + dualCoreWarning = emptyFlow(), + onSetDualCoreEnabled = {}, + onSkipDualCoreWarning = {}, onConfirmStartGame = {}, hostInputAuthorityEnabled = true, networkMode = NetworkMode.HOST_INPUT_AUTHORITY, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt index 68c859082c..47f05f14ca 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt @@ -947,7 +947,8 @@ enum class BooleanSetting( "ProgressEnabled", false ), - NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false); + NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false), + NETPLAY_SKIP_DUAL_CORE_WARNING(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "SkipDualCoreWarning", false); override val isOverridden: Boolean get() = NativeConfig.isOverridden(file, section, key) diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 9d97169d95..a6419ec83b 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1012,6 +1012,9 @@ It can efficiently compress both junk data and encrypted Wii data. Start Warning Not all players have the game. Do you really want to start? + Dual Core Enabled + Dual core during netplay is not recommended. It can cause performance issues and prevent certain games from booting. + Don\'t warn again Chat Send Game changed to %1$s