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;