summaryrefslogtreecommitdiff
path: root/Source/Android
diff options
context:
space:
mode:
authorTom Pratt <tom.pratt@outlook.com>2026-05-12 11:20:46 -0700
committerTom Pratt <tompratt@squareup.com>2026-05-19 12:02:59 +0200
commit77f08364db41de05e06d02128c3107c2d45df105 (patch)
treea1302806b82586bd567df919ca1230fe9d1fc266 /Source/Android
parentb63ccacdfa7355989e0ca971b9b06ef1dcfb3bbb (diff)
Create NetPlayServer and start game
Diffstat (limited to 'Source/Android')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/NetplaySession.kt31
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplaySetupViewModel.kt13
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/model/NetplayViewModel.kt6
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayActivity.kt4
-rw-r--r--Source/Android/jni/AndroidCommon/IDCache.cpp7
-rw-r--r--Source/Android/jni/AndroidCommon/IDCache.h1
-rw-r--r--Source/Android/jni/NetPlay/NetPlayUICallbacks.cpp2
-rw-r--r--Source/Android/jni/NetPlay/Netplay.cpp87
8 files changed, 137 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 e0531a6477..1ec663a1a5 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 isClosed = false
private set
+
+ val isHosting: Boolean
+ get() = netPlayServerPointer != 0L
val isLaunching: Boolean
get() = bootSessionDataPointer != 0L
@@ -126,10 +131,24 @@ class NetplaySession(
true
}
+ suspend fun host(): Boolean = withContext(Dispatchers.IO) {
+ if (isClosed) throw IllegalStateException("Cannot host a closed session")
+
+ 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
@@ -174,6 +193,12 @@ class NetplaySession(
nativeReleaseClient(currentNetPlayClientPointer)
}
+ val currentNetPlayServerPointer = netPlayServerPointer
+ if (currentNetPlayServerPointer != 0L) {
+ netPlayServerPointer = 0
+ nativeReleaseServer(currentNetPlayServerPointer)
+ }
+
val currentNetPlayUICallbacksPointer = netPlayUICallbacksPointer
if (currentNetPlayUICallbacksPointer != 0L) {
netPlayUICallbacksPointer = 0
@@ -187,6 +212,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)
@@ -195,8 +222,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<NetPlay::NetPlayUICallbacks*>(
- env->GetLongField(obj, IDCache::GetNetPlayUICallbacksPointer()));
+ return reinterpret_cast<NetPlay::NetPlayUICallbacks*>(
+ 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<NetPlay::NetPlayServer*>(
+ 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<jlong>(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<jlong>(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<NetPlay::NetPlayClient*>(pointer);
}
+JNIEXPORT void JNICALL
+Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseServer(JNIEnv*, jobject,
+ jlong pointer)
+{
+ delete reinterpret_cast<NetPlay::NetPlayServer*>(pointer);
+}
+
} // extern "C"