diff --git a/src/android/app/src/ea/res/drawable/ic_multiplayer.xml b/src/android/app/src/ea/res/drawable/ic_multiplayer.xml
new file mode 100644
index 0000000000..6457435a41
--- /dev/null
+++ b/src/android/app/src/ea/res/drawable/ic_multiplayer.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index c11b6bc169..022dd28fcd 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -464,6 +464,17 @@ object NativeLibrary {
*/
external fun submitInlineKeyboardInput(key_code: Int)
+ /**
+ * Connects to a room (similar with desktop version's "direct connect")
+ */
+ external fun connectToRoom(nickname: String, server_addr: String, server_port: Int, password: String)
+
+ /**
+ * Returns the state of the room member (client)
+ * @return The state as a string
+ */
+ external fun getRoomMemberState(): String
+
/**
* Button type for use in onTouchEvent
*/
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
index 9fd9add6de..2e358cf8fa 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
@@ -10,6 +10,7 @@ import android.content.Context
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.DocumentsTree
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import org.yuzu.yuzu_emu.utils.MultiplayerHelper
import org.yuzu.yuzu_emu.utils.NetworkHelper
import java.io.File
@@ -48,6 +49,7 @@ class YuzuApplication : Application() {
DirectoryInitialization.start(applicationContext)
GpuDriverHelper.initializeDriverParameters(applicationContext)
NetworkHelper.setRoutes(applicationContext)
+ MultiplayerHelper.initRoom(applicationContext)
NativeLibrary.logDeviceInfo()
createNotificationChannels();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index 4eefa0bcce..858c103fba 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -109,6 +109,7 @@ class Settings {
const val SECTION_CPU = "Cpu"
const val SECTION_NETWORK = "Network"
const val SECTION_THEME = "Theme"
+ const val SECTION_MULTIPLAYER = "Multiplayer"
const val SECTION_DEBUG = "Debug"
const val PREF_OVERLAY_INIT = "OverlayInit"
@@ -131,6 +132,12 @@ class Settings {
const val PREF_BUTTON_TOGGLE_13 = "buttonToggle13"
const val PREF_BUTTON_TOGGLE_14 = "buttonToggle14"
+ const val PREF_ROOM_ADDRESS = "MultiplayerRoom_ServerAddress"
+ const val PREF_ROOM_PORT = "MultiplayerRoom_ServerPort"
+ const val PREF_ROOM_NICKNAME = "MultiplayerRoom_Nickname"
+ const val PREF_ROOM_PASSWORD = "MultiplayerRoom_Password"
+ const val PREF_ROOM_CONNECT_ON_START = "MultiplayerRoom_ConnectOnStart"
+
const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index e64f3760b9..9f12261726 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -12,6 +12,7 @@ import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
@@ -67,6 +68,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
Settings.SECTION_RENDERER -> addGraphicsSettings(sl)
Settings.SECTION_AUDIO -> addAudioSettings(sl)
Settings.SECTION_NETWORK -> addNetworkSettings(sl)
+ Settings.SECTION_MULTIPLAYER -> addMultiplayerSettings(sl)
Settings.SECTION_THEME -> addThemeSettings(sl)
Settings.SECTION_DEBUG -> addDebugSettings(sl)
else -> {
@@ -460,6 +462,138 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
}
}
+ private fun addMultiplayerSettings(sl: ArrayList) {
+ settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_multiplayer))
+
+ sl.apply {
+ val serverAddress: AbstractStringSetting = object : AbstractStringSetting {
+ override var string: String
+ get() = preferences.getString(Settings.PREF_ROOM_ADDRESS, "") ?: ""
+ set(value) {
+ preferences.edit()
+ .putString(Settings.PREF_ROOM_ADDRESS, value)
+ .apply()
+ }
+ override val key: String? = null
+ override val section: String? = null
+ override val isRuntimeEditable: Boolean = false
+ override val valueAsString: String
+ get() = preferences.getString(Settings.PREF_ROOM_ADDRESS, "") ?: ""
+ override val defaultValue: Any = -1
+ }
+
+ val serverPort: AbstractStringSetting = object : AbstractStringSetting {
+ override var string: String
+ get() = preferences.getString(Settings.PREF_ROOM_PORT, "24872") ?: "24872"
+ set(value) {
+ preferences.edit()
+ .putString(Settings.PREF_ROOM_PORT, value)
+ .apply()
+ }
+ override val key: String? = null
+ override val section: String? = null
+ override val isRuntimeEditable: Boolean = false
+ override val valueAsString: String
+ get() = preferences.getString(Settings.PREF_ROOM_PORT, "24872") ?: "24872"
+ override val defaultValue: Any = "24872"
+ }
+
+ val nickname: AbstractStringSetting = object : AbstractStringSetting {
+ override var string: String
+ get() = preferences.getString(Settings.PREF_ROOM_NICKNAME, "") ?: ""
+ set(value) {
+ preferences.edit()
+ .putString(Settings.PREF_ROOM_NICKNAME, value)
+ .apply()
+ }
+ override val key: String? = null
+ override val section: String? = null
+ override val isRuntimeEditable: Boolean = false
+ override val valueAsString: String
+ get() = preferences.getString(Settings.PREF_ROOM_NICKNAME, "") ?: ""
+ override val defaultValue: Any = -1
+ }
+
+ val password: AbstractStringSetting = object : AbstractStringSetting {
+ override var string: String
+ get() = preferences.getString(Settings.PREF_ROOM_PASSWORD, "") ?: ""
+ set(value) {
+ preferences.edit()
+ .putString(Settings.PREF_ROOM_PASSWORD, value)
+ .apply()
+ }
+ override val key: String? = null
+ override val section: String? = null
+ override val isRuntimeEditable: Boolean = false
+ override val valueAsString: String
+ get() = preferences.getString(Settings.PREF_ROOM_PASSWORD, "") ?: ""
+ override val defaultValue: Any = -1
+ }
+
+ val connectOnStart: AbstractBooleanSetting = object : AbstractBooleanSetting {
+ override var boolean: Boolean
+ get() = preferences.getBoolean(Settings.PREF_ROOM_CONNECT_ON_START, false)
+ set(value) {
+ preferences.edit()
+ .putBoolean(Settings.PREF_ROOM_CONNECT_ON_START, value)
+ .apply()
+ }
+ override val key: String? = null
+ override val section: String? = null
+ override val isRuntimeEditable: Boolean = false
+ override val valueAsString: String
+ get() = preferences.getBoolean(Settings.PREF_ROOM_CONNECT_ON_START, false).toString()
+ override val defaultValue: Any = -1
+ }
+
+ add(
+ TextSetting(
+ serverAddress,
+ R.string.multiplayer_room_server_address,
+ 0,
+ "",
+ ""
+ )
+ )
+ add(
+ TextSetting(
+ serverPort,
+ R.string.multiplayer_room_server_port,
+ 0,
+ "",
+ ""
+ )
+ )
+ add(
+ TextSetting(
+ nickname,
+ R.string.multiplayer_room_nickname,
+ 0,
+ "",
+ ""
+ )
+ )
+ add(
+ TextSetting(
+ password,
+ R.string.multiplayer_room_password,
+ 0,
+ "",
+ ""
+ )
+ )
+ add(
+ SwitchSetting(
+ connectOnStart,
+ R.string.multiplayer_room_connect_on_start,
+ 0,
+ "",
+ false
+ )
+ )
+ }
+ }
+
private fun addDebugSettings(sl: ArrayList) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_debug))
sl.apply {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index bdc3375016..addbdd81d8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -84,6 +84,11 @@ class HomeSettingsFragment : Fragment() {
R.string.theme_and_color_description,
R.drawable.ic_palette
) { SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") },
+ HomeSetting(
+ R.string.preferences_multiplayer,
+ R.string.multiplayer_description,
+ R.drawable.ic_multiplayer
+ ) { SettingsActivity.launch(requireContext(), Settings.SECTION_MULTIPLAYER, "") },
HomeSetting(
R.string.install_gpu_driver,
R.string.install_gpu_driver_description,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MultiplayerHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MultiplayerHelper.kt
new file mode 100644
index 0000000000..167305593c
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MultiplayerHelper.kt
@@ -0,0 +1,21 @@
+package org.yuzu.yuzu_emu.utils
+
+import android.content.Context
+import androidx.preference.PreferenceManager
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+
+object MultiplayerHelper {
+ fun initRoom(context: Context) {
+ val preferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+ if(preferences.getBoolean(Settings.PREF_ROOM_CONNECT_ON_START, false)) {
+ val addr = preferences.getString(Settings.PREF_ROOM_ADDRESS, "") ?: ""
+ val port = preferences.getString(Settings.PREF_ROOM_PORT, "24872") ?: "24872"
+ val nickname = preferences.getString(Settings.PREF_ROOM_NICKNAME, "") ?: ""
+ val password = preferences.getString(Settings.PREF_ROOM_PASSWORD, "") ?: ""
+
+ NativeLibrary.connectToRoom(nickname, addr, port.toIntOrNull() ?: 0, password)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NetworkHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NetworkHelper.kt
index bfb80b5014..0e7ca5ba79 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NetworkHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NetworkHelper.kt
@@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.utils
import android.content.Context
import android.net.ConnectivityManager
-import androidx.preference.PreferenceManager
object NetworkHelper {
fun setRoutes(context: Context) {
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index b87e04b3d8..8a91c562f7 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -13,6 +13,8 @@
#include
#include
+#include
+#include
#include "common/detached_tasks.h"
#include "common/dynamic_library.h"
@@ -344,6 +346,29 @@ public:
return m_software_keyboard;
}
+ void DirectConnectToRoom(const std::string& nickname, const char* server_addr = "127.0.0.1",
+ u16 server_port = Network::DefaultRoomPort,
+ const std::string& password = "") {
+ auto room_network = m_system.GetRoomNetwork();
+
+ if (const auto member = room_network.GetRoomMember().lock()) {
+ // Prevent the user from trying to join a room while they are already joining.
+ if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) {
+ return;
+ } else {
+ member->Join(nickname, server_addr, server_port);
+ }
+ }
+ }
+
+ Network::RoomMember::State GetRoomMemberState() {
+ if (const auto member = m_system.GetRoomNetwork().GetRoomMember().lock()) {
+ return member->GetState();
+ } else {
+ return Network::RoomMember::State::Idle;
+ }
+ }
+
private:
struct RomMetadata {
std::string title;
@@ -771,4 +796,45 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env
EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code);
}
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_connectToRoom(JNIEnv* env, jclass clazz,
+ jstring nickname,
+ jstring server_addr,
+ jint server_port,
+ jstring password) {
+ EmulationSession::GetInstance().DirectConnectToRoom(
+ GetJString(env, nickname),
+ GetJString(env, server_addr).c_str(),
+ server_port,
+ GetJString(env, password)
+ );
+}
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getRoomMemberState(JNIEnv* env, jclass clazz) {
+ auto state = EmulationSession::GetInstance().GetRoomMemberState();
+
+ std::string state_str{};
+
+ switch(state) {
+ using State = Network::RoomMember::State;
+
+ case State::Uninitialized:
+ state_str = "Uninitialized";
+ break;
+ case State::Idle:
+ state_str = "Idle";
+ break;
+ case State::Joining:
+ state_str = "Joining";
+ break;
+ case State::Joined:
+ state_str = "Joined";
+ break;
+ case State::Moderator:
+ state_str = "Moderator";
+ break;
+ }
+
+ return env->NewStringUTF(state_str.c_str());
+}
+
} // extern "C"
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index a2358a93ab..350eb1dedb 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -84,6 +84,7 @@
Open yuzu folder
Manage yuzu\'s internal files
Modify the look of the app
+ Play with your friends!
No file manager found
Could not open yuzu directory
Please locate the user folder with the file manager\'s side panel manually.
@@ -155,6 +156,13 @@
Sets the default network route
Set network route
+
+ Server address
+ Port
+ Nickname
+ Password
+ Connect on start
+
API
Accuracy level
@@ -211,6 +219,7 @@
Graphics
Audio
Theme and color
+ Multiplayer
Debug
diff --git a/src/yuzu/multiplayer/direct_connect.cpp b/src/yuzu/multiplayer/direct_connect.cpp
index d71cc23a72..7ffe68bf2b 100644
--- a/src/yuzu/multiplayer/direct_connect.cpp
+++ b/src/yuzu/multiplayer/direct_connect.cpp
@@ -81,6 +81,7 @@ void DirectConnectWindow::Connect() {
}
}
}
+
if (!ui->ip->hasAcceptableInput()) {
NetworkMessage::ErrorManager::ShowError(NetworkMessage::ErrorManager::IP_ADDRESS_NOT_VALID);
return;