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/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index a6f87fc2ec..690ce325b6 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -11,6 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
+
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 f3bfbe7eb4..870c880f22 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
@@ -491,6 +491,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 4c947b7869..2b24ef2d32 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,8 @@ 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
fun Context.getPublicFilesDir() : File = getExternalFilesDir(null) ?: filesDir
@@ -46,6 +48,8 @@ class YuzuApplication : Application() {
documentsTree = DocumentsTree()
DirectoryInitialization.start(applicationContext)
GpuDriverHelper.initializeDriverParameters(applicationContext)
+ NetworkHelper.getRoute(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 6bcb7bee07..dc9df7be02 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
@@ -107,7 +107,9 @@ class Settings {
const val SECTION_RENDERER = "Renderer"
const val SECTION_AUDIO = "Audio"
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"
@@ -130,6 +132,14 @@ class Settings {
const val PREF_BUTTON_TOGGLE_13 = "buttonToggle13"
const val PREF_BUTTON_TOGGLE_14 = "buttonToggle14"
+ const val PREF_FORCE_WIFI = "Network_ForceWifi"
+
+ 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"
@@ -154,6 +164,7 @@ class Settings {
SECTION_SYSTEM,
SECTION_RENDERER,
SECTION_AUDIO,
+ SECTION_NETWORK,
SECTION_CPU
)
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
index 63f95690c7..999f080cb0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
@@ -8,7 +8,8 @@ enum class StringSetting(
override val section: String,
override val defaultValue: String
) : AbstractStringSetting {
- CUSTOM_RTC("custom_rtc", Settings.SECTION_SYSTEM, "0");
+ CUSTOM_RTC("custom_rtc", Settings.SECTION_SYSTEM, "0"),
+ NETWORK_ROUTE("network_route", Settings.SECTION_NETWORK, ";;;;");
override var string: String = defaultValue
@@ -27,7 +28,8 @@ enum class StringSetting(
companion object {
private val NOT_RUNTIME_EDITABLE = listOf(
- CUSTOM_RTC
+ CUSTOM_RTC,
+ NETWORK_ROUTE
)
fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index 07520849ec..d12b9331ee 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -35,5 +35,6 @@ abstract class SettingsItem(
const val TYPE_STRING_SINGLE_CHOICE = 5
const val TYPE_DATETIME_SETTING = 6
const val TYPE_RUNNABLE = 7
+ const val TYPE_TEXT_SETTING = 8
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/TextSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/TextSetting.kt
new file mode 100644
index 0000000000..c1fc51c16f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/TextSetting.kt
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.model.view
+
+import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
+import org.yuzu.yuzu_emu.features.settings.model.AbstractStringSetting
+
+class TextSetting(
+ setting: AbstractSetting?,
+ titleId: Int,
+ descriptionId: Int,
+ val key: String? = null,
+ private val defaultValue: String? = null
+) : SettingsItem(setting, titleId, descriptionId) {
+ override val type = TYPE_TEXT_SETTING
+
+ val value: String
+ get() = if (setting != null) {
+ val setting = setting as AbstractStringSetting
+ setting.string
+ } else {
+ defaultValue!!
+ }
+
+ fun setSelectedValue(string: String): AbstractStringSetting {
+ val stringSetting = setting as AbstractStringSetting
+ stringSetting.string = string
+ return stringSetting
+ }
+}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
index 1eb4899fcb..2dc205a7f5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -7,13 +7,14 @@ import android.content.Context
import android.content.DialogInterface
import android.icu.util.Calendar
import android.icu.util.TimeZone
+import android.text.TextWatcher
import android.text.format.DateFormat
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.setFragmentResultListener
+import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -21,6 +22,7 @@ import com.google.android.material.slider.Slider
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogEditTextBinding
import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
@@ -83,6 +85,10 @@ class SettingsAdapter(
RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
+ SettingsItem.TYPE_TEXT_SETTING -> {
+ TextSettingViewHolder(ListItemSettingBinding.inflate(inflater), this)
+ }
+
else -> {
// TODO: Create an error view since we can't return null now
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
@@ -167,6 +173,7 @@ class SettingsAdapter(
.setSelection(storedTime)
.setTitleText(R.string.select_rtc_date)
.build()
+
val timePicker: MaterialTimePicker = MaterialTimePicker.Builder()
.setTimeFormat(timeFormat)
.setHour(calendar.get(Calendar.HOUR_OF_DAY))
@@ -199,6 +206,38 @@ class SettingsAdapter(
)
}
+ fun onTextSettingClick(item: TextSetting, position: Int) {
+ clickedItem = item
+ clickedPosition = position
+ var value = item.value
+
+ val inflater = LayoutInflater.from(context)
+ val editTextBinding = DialogEditTextBinding.inflate(inflater)
+
+ editTextBinding.editText.setText(value)
+
+ editTextBinding.editText.doAfterTextChanged {
+ value = it.toString()
+ }
+
+ dialog = MaterialAlertDialogBuilder(context)
+ .setTitle(item.nameId)
+ .setView(editTextBinding.root)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ if (item.value != value) {
+ fragmentView.onSettingChanged()
+ }
+ notifyItemChanged(clickedPosition)
+
+ val setting = item.setSelectedValue(value)
+ fragmentView.putSetting(setting)
+ clickedItem = null
+ }
+ .setNegativeButton(android.R.string.cancel, defaultCancelListener)
+ .show()
+
+ }
+
fun onSliderClick(item: SliderSetting, position: Int) {
clickedItem = item
clickedPosition = position
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 b611389a15..7577a6e7a2 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
@@ -7,12 +7,12 @@ import android.content.SharedPreferences
import android.os.Build
import android.text.TextUtils
import androidx.preference.PreferenceManager
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
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 +67,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
Settings.SECTION_SYSTEM -> addSystemSettings(sl)
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 -> {
@@ -102,6 +104,13 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
Settings.SECTION_RENDERER
)
)
+ add(
+ SubmenuSetting(
+ R.string.preferences_network,
+ 0,
+ Settings.SECTION_NETWORK
+ )
+ )
add(
SubmenuSetting(
R.string.preferences_audio,
@@ -466,6 +475,179 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
}
}
+ private fun addNetworkSettings(sl: ArrayList) {
+ settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_network))
+
+ sl.apply {
+ val forceWifi: AbstractBooleanSetting = object : AbstractBooleanSetting {
+ override var boolean: Boolean
+ get() = preferences.getBoolean(Settings.PREF_FORCE_WIFI, false)
+ set(value) {
+ preferences.edit()
+ .putBoolean(Settings.PREF_FORCE_WIFI, 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_FORCE_WIFI, false).toString()
+ override val defaultValue: Any = false
+ }
+
+ add(
+ SwitchSetting(
+ forceWifi,
+ R.string.set_force_wifi,
+ R.string.force_wifi_desc,
+ "",
+ false
+ )
+ )
+ add(
+ TextSetting(
+ StringSetting.NETWORK_ROUTE,
+ R.string.set_network_route,
+ R.string.network_route_desc,
+ StringSetting.NETWORK_ROUTE.key,
+ StringSetting.NETWORK_ROUTE.defaultValue
+ )
+ )
+ }
+ }
+
+ 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/features/settings/ui/viewholder/TextSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/TextSettingViewHolder.kt
new file mode 100644
index 0000000000..9557b07fca
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/TextSettingViewHolder.kt
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
+import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
+import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
+import org.yuzu.yuzu_emu.features.settings.model.view.TextSetting
+import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+
+class TextSettingViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+ SettingViewHolder(binding.root, adapter) {
+ private lateinit var setting: TextSetting
+
+ override fun bind(item: SettingsItem) {
+ setting = item as TextSetting
+
+ binding.textSettingName.setText(item.nameId)
+ if (item.descriptionId != 0) {
+ binding.textSettingDescription.setText(item.descriptionId)
+ binding.textSettingDescription.visibility = View.VISIBLE
+ }
+ }
+
+ override fun onClick(clicked: View) {
+ if (setting.isEditable) {
+ adapter.onTextSettingClick(setting, bindingAdapterPosition)
+ }
+ }
+
+ override fun onLongClick(clicked: View): Boolean {
+ if (setting.isEditable) {
+ return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+ }
+ return false
+ }
+}
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 536163eb6e..0b102a1026 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..bec797134e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MultiplayerHelper.kt
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+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
new file mode 100644
index 0000000000..2ed12fd57e
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NetworkHelper.kt
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+import android.content.Context
+import android.net.ConnectivityManager
+import androidx.preference.PreferenceManager
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+
+object NetworkHelper {
+ /**
+ * Gets available network interface info/route info - currently the active network info.
+ * @return The route info separated by semicolons (interface, address, netmask, gateway), or null if no networks are available.
+ */
+ fun getRoute(context: Context): String? {
+ val connectivity =
+ context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+
+ val preferences = PreferenceManager.getDefaultSharedPreferences(context)
+
+ if(connectivity.isActiveNetworkMetered && preferences.getBoolean(Settings.PREF_FORCE_WIFI, false))
+ return null
+
+ val lp = connectivity.getLinkProperties(connectivity.activeNetwork) ?: return null
+
+ val ifName = lp.interfaceName
+ val addr = lp.linkAddresses[0]
+ val cidr = addr.prefixLength
+
+ val bits = 0xffffffff xor ((1 shl 32 - cidr)).toLong() - 1
+ val mask = String.format(
+ "%d.%d.%d.%d",
+ bits and 0x0000000000ff000000L shr 24,
+ bits and 0x000000000000ff0000L shr 16,
+ bits and 0x00000000000000ff00L shr 8,
+ bits and 0x0000000000000000ffL shr 0
+ )
+
+ val gw = lp.routes.last { it.isDefaultRoute }.gateway?.hostAddress
+
+ return "$ifName;$addr;$mask;$gw"
+ }
+}
\ No newline at end of file
diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp
index 43e8aa72a2..fc76a2e047 100644
--- a/src/android/app/src/main/jni/config.cpp
+++ b/src/android/app/src/main/jni/config.cpp
@@ -248,6 +248,10 @@ void Config::ReadValues() {
ReadSetting("Audio", Settings::values.audio_output_device_id);
ReadSetting("Audio", Settings::values.volume);
+ // Network
+
+ Settings::values.network_route = config->GetString("Network", "network_route", "");
+
// Miscellaneous
// log_filter has a different default here than from common
Settings::values.log_filter = "*:Info";
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index f9617202b6..011254338b 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -14,6 +14,8 @@
#include
#include
#include
+#include
+#include
#include "common/detached_tasks.h"
#include "common/dynamic_library.h"
@@ -420,6 +422,30 @@ 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;
@@ -860,4 +886,40 @@ 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/drawable/ic_multiplayer.xml b/src/android/app/src/main/res/drawable/ic_multiplayer.xml
new file mode 100644
index 0000000000..6457435a41
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_multiplayer.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index b5bc249d49..b162d4c587 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -81,6 +81,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.
@@ -157,6 +158,19 @@
Allows you to set a custom real-time clock separate from your current system time.
Set custom RTC
+
+ Sets the default network route
+ Set network route
+ Force Wi-Fi
+ Forces yuzu to use only Wi-Fi (non-metered connections in general)
+
+
+ Server address
+ Port
+ Nickname
+ Password
+ Connect on start
+
API
Accuracy level
@@ -212,9 +226,11 @@
Settings
General
System
+ Network
Graphics
Audio
Theme and color
+ Multiplayer
Debug
diff --git a/src/common/settings.h b/src/common/settings.h
index 9682281b08..0613d6f596 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -581,6 +581,7 @@ struct Values {
// Network
Setting network_interface{std::string(), "network_interface"};
+ Setting network_route{std::string(), "network_route"};
// WebService
Setting enable_telemetry{true, "enable_telemetry"};
diff --git a/src/core/internal_network/network_interface.cpp b/src/core/internal_network/network_interface.cpp
index 4c909a6d3c..da2c328adf 100644
--- a/src/core/internal_network/network_interface.cpp
+++ b/src/core/internal_network/network_interface.cpp
@@ -6,6 +6,8 @@
#include
#include
+#include
+
#include "common/bit_cast.h"
#include "common/common_types.h"
#include "common/logging/log.h"
@@ -18,8 +20,10 @@
#include
#else
#include
+#include
#include
#include
+
#endif
namespace Network {
@@ -91,8 +95,28 @@ std::vector GetAvailableNetworkInterfaces() {
return result;
}
-#else
+#elif defined(__ANDROID__)
+std::vector GetAvailableNetworkInterfaces() {
+ std::vector result;
+ std::vector route_parts;
+
+ boost::split(route_parts, Settings::values.network_route.GetValue(), boost::is_any_of(";"));
+
+ struct in_addr ip {
+ }, sm{}, gw{};
+
+ inet_pton(AF_INET, route_parts[1].c_str(), &ip);
+ inet_pton(AF_INET, route_parts[2].c_str(), &sm);
+ inet_pton(AF_INET, route_parts[3].c_str(), &gw);
+
+ result.emplace_back(
+ NetworkInterface{.name{route_parts[0]}, .ip_address{ip}, .subnet_mask{sm}, .gateway{gw}});
+
+ return result;
+}
+
+#else
std::vector GetAvailableNetworkInterfaces() {
struct ifaddrs* ifaddr = nullptr;
@@ -187,6 +211,10 @@ std::vector GetAvailableNetworkInterfaces() {
#endif
std::optional GetSelectedNetworkInterface() {
+#ifdef __ANDROID__
+ Network::SelectFirstNetworkInterface(); // TODO ANDROID
+#endif
+
const auto& selected_network_interface = Settings::values.network_interface.GetValue();
const auto network_interfaces = Network::GetAvailableNetworkInterfaces();
if (network_interfaces.empty()) {
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;