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 94de9bd98c..a90d1b00d8 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 @@ -44,6 +44,9 @@ object Netplay { private val _launchGame = Channel(Channel.CONFLATED) val launchGame = _launchGame.receiveAsFlow() + private val _stopGame = Channel(Channel.CONFLATED) + val stopGame = _stopGame.receiveAsFlow() + private val _connectionErrors = Channel(Channel.BUFFERED) val connectionErrors = _connectionErrors.receiveAsFlow() @@ -123,6 +126,7 @@ object Netplay { } _launchGame.flush() + _stopGame.flush() _connectionErrors.flush() _players.resetReplayCache() _messages.resetReplayCache() @@ -166,9 +170,16 @@ object Netplay { @JvmStatic fun onBootGame(gameFilePath: String, bootSessionDataPointer: Long) { this.bootSessionDataPointer = bootSessionDataPointer + _stopGame.flush() _launchGame.trySend(gameFilePath) } + @Keep + @JvmStatic + fun onStopGame() { + _stopGame.trySend(Unit) + } + @JvmStatic fun onConnectionError(message: String) { _connectionErrors.trySend(message) @@ -244,6 +255,6 @@ object Netplay { } } -private fun Channel.flush() { +private fun Channel.flush() { while (this.tryReceive().isSuccess) Unit } 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 4ee4aee8aa..61a5493950 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 @@ -10,6 +10,9 @@ import android.view.SurfaceHolder import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.activities.EmulationActivity import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding @@ -220,6 +223,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { val paths = requireNotNull(gamePaths) { "Cannot start emulation without any game paths" } + lifecycleScope.launch { + Netplay.stopGame.first() + stopEmulation() + } NativeLibrary.RunNetPlay(paths, riivolution) } else { Log.debug("[EmulationFragment] Starting emulation thread.") diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 0ea1b77c54..bc59fe0053 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -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_stop_game; static jmethodID s_netplay_on_connection_error; static jmethodID s_netplay_on_game_changed; static jmethodID s_netplay_on_host_input_authority_changed; @@ -267,6 +268,11 @@ jmethodID GetNetplayOnBootGame() return s_netplay_on_boot_game; } +jmethodID GetNetplayOnStopGame() +{ + return s_netplay_on_stop_game; +} + jmethodID GetNetplayOnConnectionError() { return s_netplay_on_connection_error; @@ -717,6 +723,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_stop_game = env->GetStaticMethodID(netplay_class, "onStopGame", "()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"); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index f61c0265d9..5646669f18 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -35,6 +35,7 @@ jclass GetNetplayClass(); jfieldID GetNetPlayClientPointer(); jfieldID GetNetplayBootSessionDataPointer(); jmethodID GetNetplayOnBootGame(); +jmethodID GetNetplayOnStopGame(); jmethodID GetNetplayOnConnectionError(); jmethodID GetNetplayOnGameChanged(); jmethodID GetNetplayOnHostInputAuthorityChanged(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 8f80d336b0..dd25d54280 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -4,6 +4,7 @@ #include "UICommon/GameFile.h" #include "NetPlayUICallbacks.h" #include "Core/Boot/Boot.h" +#include "Core/Core.h" #include "jni/AndroidCommon/AndroidCommon.h" #include "jni/AndroidCommon/IDCache.h" @@ -12,17 +13,38 @@ namespace NetPlay { NetPlayUICallbacks::NetPlayUICallbacks(std::vector> games) : m_games(std::move(games)) { + m_state_changed_hook = Core::AddOnStateChangedCallback([this](Core::State state) { + if ((state == Core::State::Uninitialized || state == Core::State::Stopping) && + !m_got_stop_request) + { + JNIEnv* env = IDCache::GetEnvForThread(); + auto* client = reinterpret_cast( + env->GetStaticLongField(IDCache::GetNetplayClass(), IDCache::GetNetPlayClientPointer())); + if (client) + client->RequestStopGame(); + } + }); } NetPlayUICallbacks::~NetPlayUICallbacks() = default; void NetPlayUICallbacks::BootGame(const std::string& filename, std::unique_ptr boot_session_data) { + m_got_stop_request = false; JNIEnv* env = IDCache::GetEnvForThread(); env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnBootGame(), ToJString(env, filename), reinterpret_cast(boot_session_data.release())); } -void NetPlayUICallbacks::StopGame() {} +void NetPlayUICallbacks::StopGame() +{ + if (m_got_stop_request) + return; + + m_got_stop_request = true; + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNetplayClass(), IDCache::GetNetplayOnStopGame()); +} + bool NetPlayUICallbacks::IsHosting() const { return false; } void NetPlayUICallbacks::Update() diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h index acccfccabf..10c1b1ac22 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.h +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.h @@ -5,6 +5,7 @@ #include #include +#include "Common/HookableEvent.h" #include "Core/NetPlayClient.h" #include "UICommon/GameFile.h" @@ -61,6 +62,8 @@ private: std::vector> m_games; NetPlay::SyncIdentifier m_current_game_identifier; std::string m_current_game_name; + Common::EventHook m_state_changed_hook; + bool m_got_stop_request = true; }; } // namespace NetPlay