diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt index 6dde90653f..e781d8356c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt @@ -95,6 +95,12 @@ class NetplaySession( ) val padBuffer = _padBuffer.asSharedFlow() + private val _desyncMessages = MutableSharedFlow( + extraBufferCapacity = 32, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val desyncMessages = _desyncMessages.asSharedFlow() + private val _saveTransferProgress = MutableStateFlow(null) val saveTransferProgress = _saveTransferProgress.asStateFlow() @@ -146,6 +152,7 @@ class NetplaySession( game.map { NetplayMessage.GameChanged(it) }, hostInputAuthorityEnabled.map { NetplayMessage.HostInputAuthorityChanged(it) }, padBuffer.map { NetplayMessage.BufferChanged(it) }, + desyncMessages, ) private fun releaseNativeResources() { @@ -234,6 +241,11 @@ class NetplaySession( _padBuffer.tryEmit(buffer) } + @Keep + fun onDesync(frame: Int, player: String) { + _desyncMessages.tryEmit(NetplayMessage.Desync(player, frame)) + } + @Keep fun onShowChunkedProgressDialog(title: String, dataSize: Long, playerIds: IntArray) { val players = _players.replayCache.firstOrNull() 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 index 41ab8997bd..b8291c094b 100644 --- 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 @@ -26,4 +26,9 @@ sealed class NetplayMessage { override fun message(context: Context) = context.getString(R.string.netplay_message_buffer_changed, buffer) } + + class Desync(private val player: String, private val frame: Int) : NetplayMessage() { + override fun message(context: Context) = + context.getString(R.string.netplay_message_desync, player, frame) + } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index c989cdc780..a005995bdb 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -9,9 +9,12 @@ import android.view.LayoutInflater import android.view.SurfaceHolder import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.activities.EmulationActivity @@ -231,6 +234,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { netplaySession.stopGame.first() stopEmulation() } + netplaySession + .desyncMessages + .onEach { Toast.makeText(requireContext(), it.message(requireContext()), Toast.LENGTH_SHORT).show() } + .launchIn(lifecycleScope) NativeLibrary.RunNetPlay( paths, riivolution, diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index cc1866d473..4e9c977faa 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -1002,6 +1002,7 @@ It can efficiently compress both junk data and encrypted Wii data. Game changed to %1$s Buffer size changed to %1$d "Host input authority %1$s" + Possible desync detected: %1$s might have desynced at frame %2$d Game Players Name diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 2bee9e2138..caf305fc9b 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -43,6 +43,7 @@ static jmethodID s_netplay_update; static jmethodID s_netplay_on_show_chunked_progress_dialog; static jmethodID s_netplay_on_set_chunked_progress; static jmethodID s_netplay_on_hide_chunked_progress_dialog; +static jmethodID s_netplay_on_desync; static jclass s_netplay_player_class; static jmethodID s_netplay_player_constructor; @@ -327,6 +328,11 @@ jmethodID GetNetplayOnHideChunkedProgressDialog() return s_netplay_on_hide_chunked_progress_dialog; } +jmethodID GetNetplayOnDesync() +{ + return s_netplay_on_desync; +} + jclass GetNetplayPlayerClass() { return s_netplay_player_class; @@ -766,6 +772,8 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetMethodID(netplay_class, "onSetChunkedProgress", "(IJ)V"); s_netplay_on_hide_chunked_progress_dialog = env->GetMethodID(netplay_class, "onHideChunkedProgressDialog", "()V"); + s_netplay_on_desync = + env->GetMethodID(netplay_class, "onDesync", "(ILjava/lang/String;)V"); env->DeleteLocalRef(netplay_class); const jclass netplay_player_class = diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index c5f0a45573..ae86740f09 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -46,6 +46,7 @@ jmethodID GetNetplayUpdate(); jmethodID GetNetplayOnShowChunkedProgressDialog(); jmethodID GetNetplayOnSetChunkedProgress(); jmethodID GetNetplayOnHideChunkedProgressDialog(); +jmethodID GetNetplayOnDesync(); jclass GetNetplayPlayerClass(); jmethodID GetNetplayPlayerConstructor(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 1f9e701edb..c411119ab9 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -197,7 +197,17 @@ void NetPlayUICallbacks::OnHostInputAuthorityChanged(bool enabled) env->DeleteLocalRef(netplay_session); } -void NetPlayUICallbacks::OnDesync(u32, const std::string&) {} +void NetPlayUICallbacks::OnDesync(u32 frame, const std::string& player) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + jobject netplay_session = GetNetplaySessionLocalRef(env); + if (!netplay_session) + return; + + env->CallVoidMethod(netplay_session, IDCache::GetNetplayOnDesync(), + static_cast(frame), ToJString(env, player)); + env->DeleteLocalRef(netplay_session); +} void NetPlayUICallbacks::OnConnectionLost() {