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 ceda995051..88baa9f495 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 @@ -4,19 +4,31 @@ package org.dolphinemu.dolphinemu.features.netplay import androidx.annotation.Keep +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach 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.ConnectionType +import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player +//TODO add other necessary @Keep annotations +//TODO clear boot session data at appropriate time object Netplay { @Keep private var netPlayClientPointer: Long = 0 @@ -24,6 +36,8 @@ object Netplay { @Keep private var bootSessionDataPointer: Long = 0 + private var sessionScope: CoroutineScope? = null + val isLaunching: Boolean get() = bootSessionDataPointer != 0L @@ -33,13 +47,51 @@ object Netplay { private val _connectionErrors = Channel(Channel.BUFFERED) val connectionErrors = _connectionErrors.receiveAsFlow() + private val _messages = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val messages = _messages.asSharedFlow() + private val _players = MutableSharedFlow>( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST ) val players = _players.asSharedFlow().distinctUntilChanged() + private val _chatMessages = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val chatMessages = _chatMessages.asSharedFlow() + + private val _game = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val game = _game.asSharedFlow() + + private val _hostInputAuthorityEnabled = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val hostInputAuthorityEnabled = _hostInputAuthorityEnabled.asSharedFlow() + + private val _padBuffer = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val padBuffer = _padBuffer.asSharedFlow() + suspend fun join(): Boolean = withContext(Dispatchers.IO) { + val scope = createSessionScope() + + // Gather all messages that should appear in the chat window. + mergeMessages() + .runningFold(emptyList()) { acc, msg -> listOf(msg) + acc } + .onEach { _messages.tryEmit(it) } + .launchIn(scope) + netPlayClientPointer = Join() val isConnected = netPlayClientPointer != 0L && isClientConnected() @@ -62,13 +114,29 @@ object Netplay { @OptIn(ExperimentalCoroutinesApi::class) private fun releaseNetplayClient() { + sessionScope?.cancel() + sessionScope = null + if (netPlayClientPointer != 0L) { ReleaseNetplayClient() netPlayClientPointer = 0 } + _launchGame.flush() _connectionErrors.flush() _players.resetReplayCache() + _messages.resetReplayCache() + _chatMessages.resetReplayCache() + _game.resetReplayCache() + _hostInputAuthorityEnabled.resetReplayCache() + _padBuffer.resetReplayCache() + } + + private fun createSessionScope(): CoroutineScope { + sessionScope?.cancel() + return CoroutineScope(SupervisorJob() + Dispatchers.IO).also { + sessionScope = it + } } @JvmStatic @@ -77,9 +145,19 @@ object Netplay { @JvmStatic external fun isClientConnected(): Boolean + @JvmStatic + external fun sendMessage(message: String) + @JvmStatic private external fun ReleaseNetplayClient() + private fun mergeMessages(): Flow = merge( + chatMessages.map { NetplayMessage.Chat(it) }, + game.map { NetplayMessage.GameChanged(it) }, + hostInputAuthorityEnabled.map { NetplayMessage.HostInputAuthorityChanged(it) }, + padBuffer.map { NetplayMessage.BufferChanged(it) }, + ) + // NetPlayUI callbacks @JvmStatic @@ -98,6 +176,26 @@ object Netplay { _players.tryEmit(players.toList()) } + @JvmStatic + fun onChatMessageReceived(message: String) { + _chatMessages.tryEmit(message) + } + + @JvmStatic + fun onHostInputAuthorityChanged(enabled: Boolean) { + _hostInputAuthorityEnabled.tryEmit(enabled) + } + + @JvmStatic + fun onGameChanged(game: String) { + _game.tryEmit(game) + } + + @JvmStatic + fun onPadBufferChanged(buffer: Int) { + _padBuffer.tryEmit(buffer) + } + // Settings @JvmStatic diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt new file mode 100644 index 0000000000..41ab8997bd --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayMessage.kt @@ -0,0 +1,29 @@ +package org.dolphinemu.dolphinemu.features.netplay.model + +import android.content.Context +import org.dolphinemu.dolphinemu.R + +sealed class NetplayMessage { + abstract fun message(context: Context): String + + class Chat(private val chatMessage: String) : NetplayMessage() { + override fun message(context: Context) = chatMessage + } + + class GameChanged(private val game: String) : NetplayMessage() { + override fun message(context: Context) = + context.getString(R.string.netplay_message_game_changed, game) + } + + class HostInputAuthorityChanged(private val hostInputAuthorityEnabled: Boolean) : NetplayMessage() { + override fun message(context: Context) = context.getString( + R.string.netplay_message_host_input_authority_changed, + if (hostInputAuthorityEnabled) "enabled" else "disabled" + ) + } + + class BufferChanged(private val buffer: Int) : NetplayMessage() { + override fun message(context: Context) = + context.getString(R.string.netplay_message_buffer_changed, buffer) + } +} 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 942dc0d0dc..cc9dc00ae0 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 @@ -2,6 +2,8 @@ package org.dolphinemu.dolphinemu.features.netplay.model +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -14,6 +16,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.features.netplay.Netplay +//TODO save settings class NetplayViewModel : ViewModel() { val launchGame = Netplay.launchGame @@ -23,12 +26,27 @@ class NetplayViewModel : ViewModel() { val players = Netplay.players .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + val messages = Netplay.messages + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + val game = Netplay.game + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") + init { if (!Netplay.isClientConnected()) { _goBack.trySend(Unit) } } + fun sendMessage(message: String) { + val trimmedMessage = message.trim() + if (trimmedMessage.isEmpty()) { + return + } + + Netplay.sendMessage(trimmedMessage) + } + @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() 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 5a91ae5b91..c4d0ded675 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,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { DolphinTheme { NetplayScreen( onBackClicked = { finish() }, + messages = viewModel.messages.collectAsState().value, + onSendMessage = viewModel::sendMessage, + game = viewModel.game.collectAsState().value, players = viewModel.players.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 c94579d181..d5fdab900a 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 @@ -16,7 +16,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +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 @@ -42,12 +45,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage import org.dolphinemu.dolphinemu.features.netplay.model.Player import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer @@ -58,6 +64,9 @@ import org.dolphinemu.dolphinemu.ui.theme.PreviewTheme @Composable fun NetplayScreen( onBackClicked: () -> Unit, + messages: List, + onSendMessage: (String) -> Unit, + game: String, players: List, ) { Scaffold( @@ -82,11 +91,17 @@ fun NetplayScreen( if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE) { LandscapeContent( + messages = messages, + onSendMessage = onSendMessage, + game = game, players = players, modifier = modifier ) } else { PortraitContent( + messages = messages, + onSendMessage = onSendMessage, + game = game, players = players, modifier = modifier ) @@ -96,6 +111,9 @@ fun NetplayScreen( @Composable private fun PortraitContent( + messages: List, + onSendMessage: (String) -> Unit, + game: String, players: List, modifier: Modifier = Modifier, ) { @@ -103,6 +121,8 @@ private fun PortraitContent( modifier = modifier ) { Chat( + messages = messages, + onSendMessage = onSendMessage, modifier = Modifier .fillMaxWidth() .fillMaxHeight(0.3f) @@ -112,6 +132,7 @@ private fun PortraitContent( MenuSpacer() PLayersAndSettings( + game = game, players = players, modifier = Modifier .weight(1f) @@ -122,6 +143,9 @@ private fun PortraitContent( @Composable private fun LandscapeContent( + messages: List, + onSendMessage: (String) -> Unit, + game: String, players: List, modifier: Modifier = Modifier, ) { @@ -129,6 +153,8 @@ private fun LandscapeContent( modifier = modifier ) { Chat( + messages = messages, + onSendMessage = onSendMessage, modifier = Modifier .weight(1f) .fillMaxHeight() @@ -136,6 +162,7 @@ private fun LandscapeContent( ) PLayersAndSettings( + game = game, players = players, modifier = Modifier .weight(1f) @@ -146,6 +173,7 @@ private fun LandscapeContent( @Composable private fun PLayersAndSettings( + game: String, players: List, modifier: Modifier = Modifier, ) { @@ -153,6 +181,17 @@ private fun PLayersAndSettings( modifier = modifier .verticalScroll(rememberScrollState()) ) { + OutlinedTextField( + value = game, + onValueChange = {}, + label = { Text(stringResource(R.string.netplay_game_label)) }, + readOnly = true, + modifier = Modifier + .fillMaxWidth() + ) + + MenuSpacer() + PlayersTable( rows = buildList { add(listOf("Player", "Ping", "Mapping")) @@ -168,8 +207,24 @@ private fun PLayersAndSettings( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun Chat( + messages: List, + onSendMessage: (String) -> Unit, modifier: Modifier, ) { + val context = LocalContext.current + + fun LazyListScope.messages() { + items(messages.size) { index -> + Text(text = messages[index].message(context)) + } + } + + var draftMessage by remember { mutableStateOf("") } + val submitMessage = { + onSendMessage(draftMessage) + draftMessage = "" + } + var showBottomSheet by remember { mutableStateOf(false) } val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -187,6 +242,7 @@ private fun Chat( .weight(1f) .padding(horizontal = DolphinTheme.scaffoldPadding) ) { + messages() } Row( @@ -197,19 +253,23 @@ private fun Chat( .padding(horizontal = 8.dp) ) { OutlinedTextField( - value = "", - onValueChange = {}, + value = draftMessage, + onValueChange = { draftMessage = it }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), + keyboardActions = KeyboardActions(onSend = { submitMessage() }), modifier = Modifier .weight(1f) ) TextButton( - onClick = {}, + onClick = submitMessage, + enabled = draftMessage.isNotBlank(), ) { Text(stringResource(R.string.netplay_chat_send)) } } } } + OutlinedBox( onClick = { showBottomSheet = true }, label = { Text(stringResource(R.string.netplay_chat_label)) }, @@ -221,6 +281,7 @@ private fun Chat( modifier = Modifier .fillMaxSize() ) { + messages() } } } @@ -286,10 +347,7 @@ private fun PlayersTable( @Composable private fun NetplayScreenPreview() { PreviewTheme(darkTheme = false) { - NetplayScreen( - onBackClicked = {}, - players = previewPlayers, - ) + PreviewNetplayScreen() } } @@ -297,10 +355,7 @@ private fun NetplayScreenPreview() { @Composable private fun NetplayScreenDarkPreview() { PreviewTheme(darkTheme = true) { - NetplayScreen( - onBackClicked = {}, - players = previewPlayers, - ) + PreviewNetplayScreen() } } @@ -308,10 +363,7 @@ private fun NetplayScreenDarkPreview() { @Composable private fun LandscapeNetplayScreenPreview() { PreviewTheme(darkTheme = false) { - NetplayScreen( - onBackClicked = {}, - players = previewPlayers, - ) + PreviewNetplayScreen() } } @@ -320,31 +372,42 @@ private fun LandscapeNetplayScreenPreview() { heightDp = 411, uiMode = Configuration.UI_MODE_NIGHT_YES ) + @Composable private fun LandscapeNetplayScreenDarkPreview() { PreviewTheme(darkTheme = true) { - NetplayScreen( - onBackClicked = {}, - players = previewPlayers, - ) + PreviewNetplayScreen() } } -private val previewPlayers = listOf( - Player( - pid = 1, - name = "Player 1", - revision = "123", - ping = 2, - isHost = true, - mapping = "m1" - ), - Player( - pid = 2, - name = "Player 2", - revision = "123", - ping = 23, - isHost = false, - mapping = "m2" - ), -) +@Composable +private fun PreviewNetplayScreen() { + NetplayScreen( + onBackClicked = {}, + players = listOf( + Player( + pid = 1, + name = "Player 1", + revision = "123", + ping = 2, + isHost = true, + mapping = "m1" + ), + Player( + pid = 2, + name = "Player 2", + revision = "123", + ping = 23, + isHost = false, + mapping = "m2" + ), + ), + messages = buildList { + repeat(5) { + add(NetplayMessage.Chat("Hello")) + } + }, + onSendMessage = {}, + game = "Game name", + ) +} diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index bd1acb0bfb..fa2d588652 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -999,4 +999,8 @@ It can efficiently compress both junk data and encrypted Wii data. Start Chat Send + Game changed to %1$s + Buffer size changed to %1$d + "Host input authority %1$s" + Game diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index b45849bf5d..0ea1b77c54 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -33,6 +33,10 @@ 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 jmethodID s_netplay_on_game_changed; +static jmethodID s_netplay_on_host_input_authority_changed; +static jmethodID s_netplay_on_pad_buffer_changed; +static jmethodID s_netplay_on_chat_message_received; static jmethodID s_netplay_update; static jclass s_netplay_player_class; @@ -268,6 +272,26 @@ jmethodID GetNetplayOnConnectionError() return s_netplay_on_connection_error; } +jmethodID GetNetplayOnGameChanged() +{ + return s_netplay_on_game_changed; +} + +jmethodID GetNetplayOnHostInputAuthorityChanged() +{ + return s_netplay_on_host_input_authority_changed; +} + +jmethodID GetNetplayOnPadBufferChanged() +{ + return s_netplay_on_pad_buffer_changed; +} + +jmethodID GetNetplayOnChatMessageReceived() +{ + return s_netplay_on_chat_message_received; +} + jmethodID GetNetplayUpdate() { return s_netplay_update; @@ -694,6 +718,14 @@ 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_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"); + s_netplay_on_host_input_authority_changed = + env->GetStaticMethodID(netplay_class, "onHostInputAuthorityChanged", "(Z)V"); + s_netplay_on_pad_buffer_changed = + env->GetStaticMethodID(netplay_class, "onPadBufferChanged", "(I)V"); + s_netplay_on_chat_message_received = + env->GetStaticMethodID(netplay_class, "onChatMessageReceived", "(Ljava/lang/String;)V"); s_netplay_update = env->GetStaticMethodID(netplay_class, "onUpdate", "([Lorg/dolphinemu/dolphinemu/features/netplay/model/Player;)V"); env->DeleteLocalRef(netplay_class); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 0e393f919a..f61c0265d9 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -36,6 +36,10 @@ jfieldID GetNetPlayClientPointer(); jfieldID GetNetplayBootSessionDataPointer(); jmethodID GetNetplayOnBootGame(); jmethodID GetNetplayOnConnectionError(); +jmethodID GetNetplayOnGameChanged(); +jmethodID GetNetplayOnHostInputAuthorityChanged(); +jmethodID GetNetplayOnPadBufferChanged(); +jmethodID GetNetplayOnChatMessageReceived(); jmethodID GetNetplayUpdate(); jclass GetNetplayPlayerClass(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 478052e36c..ee8443b958 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -59,13 +59,23 @@ void NetPlayUICallbacks::Update() env->DeleteLocalRef(player_array); } -void NetPlayUICallbacks::AppendChat(const std::string&) {} +void NetPlayUICallbacks::AppendChat(const std::string& message) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), + IDCache::GetNetplayOnChatMessageReceived(), + ToJString(env, message)); +} void NetPlayUICallbacks::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier, const std::string& netplay_name) { m_current_game_identifier = sync_identifier; m_current_game_name = netplay_name; + + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnGameChanged(), + ToJString(env, netplay_name)); } void NetPlayUICallbacks::OnMsgChangeGBARom(int, const NetPlay::GBAConfig&) {} @@ -86,8 +96,23 @@ void NetPlayUICallbacks::OnMsgStopGame() {} void NetPlayUICallbacks::OnMsgPowerButton() {} void NetPlayUICallbacks::OnPlayerConnect(const std::string&) {} void NetPlayUICallbacks::OnPlayerDisconnect(const std::string&) {} -void NetPlayUICallbacks::OnPadBufferChanged(u32) {} -void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool) {} + +void NetPlayUICallbacks::OnPadBufferChanged(u32 buffer) +{ + //TODO handle host input authority = true + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnPadBufferChanged(), + static_cast(buffer)); +} + +void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), + IDCache::GetNetplayOnHostInputAuthorityChanged(), + static_cast(enabled)); +} + void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} void NetPlayUICallbacks::OnConnectionLost() {} diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index c5fdd21ad5..75fb10ef31 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -136,6 +136,14 @@ Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_isClientConnected(JNIEnv return static_cast(GetPointer(env)->IsConnected()); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_sendMessage(JNIEnv* env, jclass, + jstring jmessage) +{ + if (auto* client = GetPointer(env)) + client->SendChatMessage(GetJString(env, jmessage)); +} + JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_Netplay_Join(JNIEnv* env, jclass) {