From fa5facfdfb49ef276a8fba0e587f2a8dd5a29ce7 Mon Sep 17 00:00:00 2001 From: Tom Pratt Date: Sat, 2 May 2026 20:04:06 +0200 Subject: [PATCH] Create NetPlayServer and start game --- .../features/netplay/NetplaySession.kt | 29 +++++++ .../netplay/model/NetplaySetupViewModel.kt | 13 +-- .../netplay/model/NetplayViewModel.kt | 6 ++ .../features/netplay/ui/NetplayActivity.kt | 4 +- Source/Android/jni/AndroidCommon/IDCache.cpp | 7 ++ Source/Android/jni/AndroidCommon/IDCache.h | 1 + .../jni/NetPlay/NetPlayUICallbacks.cpp | 2 + Source/Android/jni/NetPlay/Netplay.cpp | 87 +++++++++++++++++-- 8 files changed, 135 insertions(+), 14 deletions(-) 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 f94c4b75f1..ab18379495 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 @@ -37,6 +37,8 @@ class NetplaySession( private var netPlayClientPointer: Long = 0 + private var netPlayServerPointer: Long = 0 + private var bootSessionDataPointer: Long = 0 private val sessionScope = CoroutineScope(SupervisorJob()) @@ -44,6 +46,9 @@ class NetplaySession( @Volatile var isClosing = false private set + + val isHosting: Boolean + get() = netPlayServerPointer != 0L val isLaunching: Boolean get() = bootSessionDataPointer != 0L @@ -124,10 +129,22 @@ class NetplaySession( true } + suspend fun host(): Boolean = withContext(Dispatchers.IO) { + netPlayServerPointer = nativeHost() + if (netPlayServerPointer == 0L || !isActive) { + closeBlocking() + return@withContext false + } + + join() + } + fun sendMessage(message: String) = nativeSendMessage(message) fun adjustPadBufferSize(buffer: Int) = nativeAdjustPadBufferSize(buffer) + fun startGame() = nativeStartGame() + fun consumeBootSessionData(): Long { return bootSessionDataPointer.also { bootSessionDataPointer = 0 @@ -172,6 +189,12 @@ class NetplaySession( nativeReleaseClient(currentNetPlayClientPointer) } + val currentNetPlayServerPointer = netPlayServerPointer + if (currentNetPlayServerPointer != 0L) { + netPlayServerPointer = 0 + nativeReleaseServer(currentNetPlayServerPointer) + } + val currentNetPlayUICallbacksPointer = netPlayUICallbacksPointer if (currentNetPlayUICallbacksPointer != 0L) { netPlayUICallbacksPointer = 0 @@ -185,6 +208,8 @@ class NetplaySession( private external fun nativeJoin(): Long + private external fun nativeHost(): Long + private external fun nativeSendMessage(message: String) private external fun nativeAdjustPadBufferSize(buffer: Int) @@ -193,8 +218,12 @@ class NetplaySession( private external fun nativeReleaseClient(pointer: Long) + private external fun nativeReleaseServer(pointer: Long) + private external fun nativeReleaseBootSessionData(pointer: Long) + private external fun nativeStartGame() + // NetPlayUI callbacks @Keep diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt index d51ae477ac..9c667245e5 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt @@ -119,11 +119,9 @@ class NetplaySetupViewModel( BooleanSetting.NETPLAY_USE_UPNP.setBoolean(NativeConfig.LAYER_BASE, useUpnp) } - fun host() { + fun host() = connect(host = true) - } - - fun connect() { + fun connect(host: Boolean = false) { if (_connecting.value) return _connecting.value = true @@ -139,7 +137,12 @@ class NetplaySetupViewModel( .onEach { _errors.emit(it) } .launchIn(this) - if (session.join()) { + val success = if (host) { + session.host() + } else { + session.join() + } + if (success) { _showNetplayScreen.trySend(Unit) } } finally { 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 d0f86ed3f6..e7518bdf43 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 @@ -22,6 +22,8 @@ class NetplayViewModel( val launchGame = netplaySession.launchGame + val isHosting = netplaySession.isHosting + val connectionLost = netplaySession.connectionLost val players = netplaySession.players @@ -43,6 +45,10 @@ class NetplayViewModel( val gameDigestProgress = netplaySession.gameDigestProgress + fun startGame() { + netplaySession.startGame() + } + fun sendMessage(message: String) { val trimmedMessage = message.trim() if (trimmedMessage.isEmpty()) { 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 c01ec2a76c..ce1f7dec19 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 @@ -51,8 +51,8 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider { messages = viewModel.messages.collectAsState().value, onSendMessage = viewModel::sendMessage, game = viewModel.game.collectAsState().value, - isHosting = false, - onStartGame = {}, + isHosting = viewModel.isHosting, + onStartGame = viewModel::startGame, players = viewModel.players.collectAsState().value, hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value, maxBuffer = viewModel.maxBuffer.collectAsState().value, diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 91f7599078..41d7055471 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -31,6 +31,7 @@ static jfieldID s_game_file_cache_manager_instance; static jclass s_netplay_class; static jfieldID s_net_play_ui_callbacks_pointer; static jfieldID s_net_play_client_pointer; +static jfieldID s_net_play_server_pointer; static jmethodID s_netplay_on_boot_game; static jmethodID s_netplay_on_stop_game; static jmethodID s_netplay_on_connection_lost; @@ -272,6 +273,11 @@ jfieldID GetNetPlayClientPointer() return s_net_play_client_pointer; } +jfieldID GetNetPlayServerPointer() +{ + return s_net_play_server_pointer; +} + jmethodID GetNetplayOnBootGame() { return s_netplay_on_boot_game; @@ -777,6 +783,7 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) s_net_play_ui_callbacks_pointer = env->GetFieldID(netplay_class, "netPlayUICallbacksPointer", "J"); s_net_play_client_pointer = env->GetFieldID(netplay_class, "netPlayClientPointer", "J"); + s_net_play_server_pointer = env->GetFieldID(netplay_class, "netPlayServerPointer", "J"); s_netplay_on_boot_game = env->GetMethodID(netplay_class, "onBootGame", "(Ljava/lang/String;J)V"); s_netplay_on_stop_game = env->GetMethodID(netplay_class, "onStopGame", "()V"); s_netplay_on_connection_lost = env->GetMethodID(netplay_class, "onConnectionLost", "()V"); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index f610380908..51a5ae02bc 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -34,6 +34,7 @@ jfieldID GetGameFileCacheManagerInstance(); jclass GetNetplayClass(); jfieldID GetNetPlayUICallbacksPointer(); jfieldID GetNetPlayClientPointer(); +jfieldID GetNetPlayServerPointer(); jmethodID GetNetplayOnBootGame(); jmethodID GetNetplayOnStopGame(); jmethodID GetNetplayOnConnectionLost(); diff --git a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp index 2ea0b814a7..ecbf168fb5 100644 --- a/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp +++ b/Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp @@ -78,6 +78,8 @@ void NetPlayUICallbacks::StopGame() env->DeleteLocalRef(netplay_session); } +// Only used by Qt UI code, never by the C++ core. On Android, hosting state +// is tracked in Kotlin (NetplaySession.isHosting). bool NetPlayUICallbacks::IsHosting() const { return false; } void NetPlayUICallbacks::Update() diff --git a/Source/Android/jni/NetPlay/Netplay.cpp b/Source/Android/jni/NetPlay/Netplay.cpp index 61c2e77b9f..1dd511954f 100644 --- a/Source/Android/jni/NetPlay/Netplay.cpp +++ b/Source/Android/jni/NetPlay/Netplay.cpp @@ -11,6 +11,7 @@ #include "Core/Boot/Boot.h" #include "Core/Config/NetplaySettings.h" #include "Core/NetPlayClient.h" +#include "Core/NetPlayServer.h" #include "UICommon/GameFile.h" #include "UICommon/GameFileCache.h" @@ -20,8 +21,8 @@ static NetPlay::NetPlayUICallbacks* GetUICallbacksPointer(JNIEnv* env, jobject obj) { - return reinterpret_cast( - env->GetLongField(obj, IDCache::GetNetPlayUICallbacksPointer())); + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetNetPlayUICallbacksPointer())); } static NetPlay::NetPlayClient* GetClientPointer(JNIEnv* env, jobject obj) @@ -30,6 +31,12 @@ static NetPlay::NetPlayClient* GetClientPointer(JNIEnv* env, jobject obj) env->GetLongField(obj, IDCache::GetNetPlayClientPointer())); } +static NetPlay::NetPlayServer* GetServerPointer(JNIEnv* env, jobject obj) +{ + return reinterpret_cast( + env->GetLongField(obj, IDCache::GetNetPlayServerPointer())); +} + extern "C" { JNIEXPORT void JNICALL @@ -74,11 +81,25 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeJoin(JNIEnv const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); const std::string nickname = Config::Get(Config::NETPLAY_NICKNAME); - const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); - const bool is_traversal = traversal_choice == "traversal"; - const std::string host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : - Config::Get(Config::NETPLAY_ADDRESS); - const u16 host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); + std::string host_ip; + u16 host_port; + bool is_traversal; + + // When hosting, join our own server on localhost + if (auto* server = GetServerPointer(env, obj)) + { + host_ip = "127.0.0.1"; + host_port = server->GetPort(); + is_traversal = false; + } + else + { + const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); + is_traversal = traversal_choice == "traversal"; + host_ip = is_traversal ? Config::Get(Config::NETPLAY_HOST_CODE) : + Config::Get(Config::NETPLAY_ADDRESS); + host_port = Config::Get(Config::NETPLAY_CONNECT_PORT); + } auto* client = new NetPlay::NetPlayClient( host_ip, host_port, ui, nickname, @@ -93,6 +114,51 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeJoin(JNIEnv return reinterpret_cast(client); } +JNIEXPORT jlong JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeHost(JNIEnv* env, jobject obj) +{ + auto* ui = GetUICallbacksPointer(env, obj); + + const std::string traversal_choice = Config::Get(Config::NETPLAY_TRAVERSAL_CHOICE); + const bool is_traversal = traversal_choice == "traversal"; + const bool use_upnp = Config::Get(Config::NETPLAY_USE_UPNP); + const std::string traversal_host = Config::Get(Config::NETPLAY_TRAVERSAL_SERVER); + const u16 traversal_port = Config::Get(Config::NETPLAY_TRAVERSAL_PORT); + const u16 traversal_port_alt = Config::Get(Config::NETPLAY_TRAVERSAL_PORT_ALT); + + const u16 host_port = is_traversal ? Config::Get(Config::NETPLAY_LISTEN_PORT) + : Config::Get(Config::NETPLAY_HOST_PORT); + + auto* server = new NetPlay::NetPlayServer( + host_port, use_upnp, ui, + NetPlay::NetTraversalConfig{is_traversal, traversal_host, traversal_port, traversal_port_alt}); + + if (!server->is_connected) + { + delete server; + return 0; + } + + const std::string network_mode = Config::Get(Config::NETPLAY_NETWORK_MODE); + const bool host_input_authority = + network_mode == "hostinputauthority" || network_mode == "golf"; + server->SetHostInputAuthority(host_input_authority); + server->AdjustPadBufferSize(Config::Get(Config::NETPLAY_BUFFER_SIZE)); + + return reinterpret_cast(server); +} + +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeStartGame(JNIEnv* env, + jobject obj) +{ + auto* server = GetServerPointer(env, obj); + if (!server) + return; + + server->RequestStartGame(); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(JNIEnv*, jobject, @@ -116,4 +182,11 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseClie delete reinterpret_cast(pointer); } +JNIEXPORT void JNICALL +Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseServer(JNIEnv*, jobject, + jlong pointer) +{ + delete reinterpret_cast(pointer); +} + } // extern "C"