Added the public lobby to android. (#125)

This is adapted from kleidis old PR to Azahar. Changes from it:
- Fixed inconsistent button styling in the dialog for connection
- Allowed to hide both empty and full rooms.
- Proper serving of preferred games
- Enables web service for android by default
- Better implementation of multiplayer.cpp that works with oop

Also fixes the room network class and turns it into a static namespace
in network

Signed-off-by: Aleksandr Popovich <alekpopo@pm.me>

Co-authored-by: swurl <swurl@swurl.xyz>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/125
Co-authored-by: Aleksandr Popovich <alekpopo@pm.me>
Co-committed-by: Aleksandr Popovich <alekpopo@pm.me>
This commit is contained in:
Aleksandr Popovich 2025-06-05 18:59:47 +00:00 committed by crueter
parent 7e13da47af
commit 76fa525592
99 changed files with 1470 additions and 498 deletions

View file

@ -1,4 +1,7 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
import android.annotation.SuppressLint
@ -162,7 +165,7 @@ android {
arguments(
"-DENABLE_QT=0", // Don't use QT
"-DENABLE_SDL2=0", // Don't use SDL
"-DENABLE_WEB_SERVICE=0", // Don't use telemetry
"-DENABLE_WEB_SERVICE=1", // Enable web service
"-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
"-DYUZU_USE_BUNDLED_VCPKG=ON",
"-DYUZU_USE_BUNDLED_FFMPEG=ON",
@ -176,9 +179,9 @@ android {
}
}
tasks.create<Delete>("ktlintReset") {
delete(File(buildDir.path + File.separator + "intermediates/ktLint"))
}
tasks.register<Delete>("ktlintReset", fun Delete.() {
delete(File(layout.buildDirectory.toString() + File.separator + "intermediates/ktLint"))
})
val showFormatHelp = {
logger.lifecycle(

View file

@ -3,6 +3,9 @@
<!--
SPDX-FileCopyrightText: 2023 yuzu Emulator Project
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: 2025 Eden Emulator Project
SPDX-License-Identifier: GPL-3.0-or-later
-->
<!--
SPDX-FileCopyrightText: Eden Emulator Project
@ -15,6 +18,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
@ -270,8 +270,7 @@ object NativeLibrary {
NetPlayManager.clearChat()
}
external fun netPlayInit()
external fun initMultiplayer()
@Keep
@JvmStatic

View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.adapters

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

View file

@ -0,0 +1,252 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.dialogs
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.core.content.getSystemService
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import info.debatty.java.stringsimilarity.Jaccard
import info.debatty.java.stringsimilarity.JaroWinkler
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogLobbyBrowserBinding
import org.yuzu.yuzu_emu.databinding.ItemLobbyRoomBinding
import org.yuzu.yuzu_emu.network.NetPlayManager
import java.util.Locale
class LobbyBrowser(context: Context) : BottomSheetDialog(context) {
private lateinit var binding: DialogLobbyBrowserBinding
private lateinit var adapter: LobbyRoomAdapter
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed =
context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
binding = DialogLobbyBrowserBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.emptyRefreshButton.setOnClickListener {
binding.progressBar.visibility = View.VISIBLE
refreshRoomList()
}
setupRecyclerView()
setupRefreshButton()
refreshRoomList()
setupSearchBar()
}
private fun setupRecyclerView() {
adapter = LobbyRoomAdapter { room -> handleRoomSelection(room) }
binding.roomList.apply {
layoutManager = LinearLayoutManager(context)
adapter = this@LobbyBrowser.adapter
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
}
private fun setupRefreshButton() {
binding.refreshButton.setOnClickListener {
binding.refreshButton.isEnabled = false
binding.progressBar.visibility = View.VISIBLE
refreshRoomList()
}
}
private fun setupSearchBar() {
binding.chipHideFull.setOnCheckedChangeListener { _, _ -> adapter.filterAndSearch() }
binding.chipHideEmpty.setOnCheckedChangeListener { _, _ -> adapter.filterAndSearch() }
binding.searchText.doOnTextChanged { text: CharSequence?, _: Int, _: Int, _: Int ->
if (text.toString().isNotEmpty()) {
binding.clearButton.visibility = View.VISIBLE
} else {
binding.clearButton.visibility = View.INVISIBLE
}
}
binding.searchText.setOnEditorActionListener { v, action, _ ->
if (action == EditorInfo.IME_ACTION_DONE) {
v.clearFocus()
val imm = context.getSystemService<InputMethodManager>()
imm?.hideSoftInputFromWindow(v.windowToken, 0)
adapter.filterAndSearch()
true
} else {
false
}
}
binding.btnSubmit.setOnClickListener { adapter.filterAndSearch() }
binding.clearButton.setOnClickListener {
binding.searchText.setText("")
adapter.updateRooms(NetPlayManager.getPublicRooms())
}
}
private fun refreshRoomList() {
NetPlayManager.refreshRoomListAsync { rooms ->
binding.emptyView.visibility = if (rooms.isEmpty()) View.VISIBLE else View.GONE
binding.roomList.visibility = if (rooms.isEmpty()) View.GONE else View.VISIBLE
binding.appbar.visibility = if (rooms.isEmpty()) View.GONE else View.VISIBLE
adapter.updateRooms(rooms)
adapter.filterAndSearch()
binding.refreshButton.isEnabled = true
binding.progressBar.visibility = View.GONE
}
}
private fun handleRoomSelection(room: NetPlayManager.RoomInfo) {
if (room.hasPassword) {
showPasswordDialog(room)
} else {
joinRoom(room, "")
}
}
private fun showPasswordDialog(room: NetPlayManager.RoomInfo) {
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_password_input, null)
val passwordInput = dialogView.findViewById<TextInputEditText>(R.id.password_input)
MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.multiplayer_password_required))
.setView(dialogView)
.setPositiveButton(R.string.multiplayer_join_room) { _, _ ->
joinRoom(room, passwordInput.text.toString())
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun joinRoom(room: NetPlayManager.RoomInfo, password: String) {
val username = NetPlayManager.getUsername(context)
Thread {
val result = NetPlayManager.netPlayJoinRoom(room.ip, room.port, username, password)
handler.post {
if (result == 0) {
dismiss()
NetPlayDialog(context).show()
}
}
}.start()
}
inner class LobbyRoomAdapter(private val onRoomSelected: (NetPlayManager.RoomInfo) -> Unit) :
RecyclerView.Adapter<LobbyRoomAdapter.RoomViewHolder>() {
private val rooms = mutableListOf<NetPlayManager.RoomInfo>()
inner class RoomViewHolder(private val binding: ItemLobbyRoomBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(room: NetPlayManager.RoomInfo) {
binding.roomName.text = room.name
binding.roomOwner.text = room.owner
binding.playerCount.text = context.getString(
R.string.multiplayer_player_count,
room.members.size,
room.maxPlayers
)
binding.lockIcon.visibility = if (room.hasPassword) View.VISIBLE else View.GONE
if (room.preferredGameName.isNotEmpty() && room.preferredGameId != 0L) {
binding.gameName.text = room.preferredGameName
} else {
binding.gameName.text = context.getString(R.string.multiplayer_no_game_info)
}
itemView.setOnClickListener { onRoomSelected(room) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomViewHolder {
val binding = ItemLobbyRoomBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return RoomViewHolder(binding)
}
override fun onBindViewHolder(holder: RoomViewHolder, position: Int) {
holder.bind(rooms[position])
}
override fun getItemCount() = rooms.size
@SuppressLint("NotifyDataSetChanged")
fun updateRooms(newRooms: List<NetPlayManager.RoomInfo>) {
rooms.clear()
rooms.addAll(newRooms)
notifyDataSetChanged()
}
fun filterAndSearch() {
if (binding.searchText.text.toString().isEmpty() &&
!binding.chipHideFull.isChecked && !binding.chipHideEmpty.isChecked
) {
adapter.updateRooms(NetPlayManager.getPublicRooms())
return
}
val baseList = NetPlayManager.getPublicRooms()
val filteredList = baseList.filter { room ->
(!binding.chipHideFull.isChecked || room.members.size < room.maxPlayers) &&
(!binding.chipHideEmpty.isChecked || room.members.isNotEmpty())
}
if (binding.searchText.text.toString().isEmpty() &&
(binding.chipHideFull.isChecked || binding.chipHideEmpty.isChecked)
) {
adapter.updateRooms(filteredList)
return
}
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
val sortedList: List<NetPlayManager.RoomInfo> = filteredList.mapNotNull { room ->
val roomName = room.name.lowercase(Locale.getDefault())
val score = searchAlgorithm.similarity(roomName, searchTerm)
if (score > 0.03) {
ScoreItem(score, room)
} else {
null
}
}.sortedByDescending { it ->
it.score
}.map { it.item }
adapter.updateRooms(sortedList)
}
}
private inner class ScoreItem(val score: Double, val item: NetPlayManager.RoomInfo)
}

View file

@ -1,12 +1,9 @@
// Copyright 2024 Mandarine Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.dialogs
import android.annotation.SuppressLint
import android.content.Context
import org.yuzu.yuzu_emu.R
import android.content.res.Configuration
@ -16,6 +13,7 @@ import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.PopupMenu
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
@ -32,10 +30,13 @@ import org.yuzu.yuzu_emu.databinding.ItemButtonNetplayBinding
import org.yuzu.yuzu_emu.databinding.ItemTextNetplayBinding
import org.yuzu.yuzu_emu.utils.CompatUtils
import org.yuzu.yuzu_emu.network.NetPlayManager
import org.yuzu.yuzu_emu.utils.GameHelper
class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
private lateinit var adapter: NetPlayAdapter
private val gameNameList: MutableList<Array<String>> = mutableListOf()
private val gameIdList: MutableList<Array<Long>> = mutableListOf()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -71,6 +72,17 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
else -> {
DialogMultiplayerConnectBinding.inflate(layoutInflater).apply {
setContentView(root)
for (game in GameHelper.cachedGameList) {
val gameName = game.title
if (gameNameList.none { it[0] == gameName }) {
gameNameList.add(arrayOf(gameName))
}
val gameId = game.programId.toLong()
if (gameIdList.none { it[0] == gameId }) {
gameIdList.add(arrayOf(gameId))
}
}
btnCreate.setOnClickListener {
showNetPlayInputDialog(true)
dismiss()
@ -79,6 +91,10 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
showNetPlayInputDialog(false)
dismiss()
}
btnLobbyBrowser.setOnClickListener {
LobbyBrowser(context).show()
dismiss()
}
}
}
}
@ -208,10 +224,11 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
override fun getItemCount() = netPlayItems.size
}
@SuppressLint("NotifyDataSetChanged")
fun refreshAdapterItems() {
val handler = Handler(Looper.getMainLooper())
NetPlayManager.setOnAdapterRefreshListener() { type, msg ->
NetPlayManager.setOnAdapterRefreshListener() { _, _ ->
handler.post {
adapter.netPlayItems.clear()
adapter.loadMultiplayerMenu()
@ -244,6 +261,17 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
binding.ipPort.setText(NetPlayManager.getRoomPort(activity))
binding.username.setText(NetPlayManager.getUsername(activity))
binding.dropdownPreferredGameName.apply {
setAdapter(
ArrayAdapter(
activity,
R.layout.dropdown_item,
gameNameList.map { it[0] }
)
)
}
binding.preferredGameName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
binding.roomName.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
binding.maxPlayersContainer.visibility = if (isCreateRoom) View.VISIBLE else View.GONE
binding.maxPlayersLabel.text = context.getString(R.string.multiplayer_max_players_value, binding.maxPlayers.value.toInt())
@ -259,6 +287,8 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
val ipAddress = binding.ipAddress.text.toString()
val username = binding.username.text.toString()
val portStr = binding.ipPort.text.toString()
val preferredGameName = binding.dropdownPreferredGameName.text.toString()
val preferredGameId = gameIdList[gameNameList.indexOfFirst { it[0] == preferredGameName }][0]
val password = binding.password.text.toString()
val port = portStr.toIntOrNull() ?: run {
Toast.makeText(activity, R.string.multiplayer_port_invalid, Toast.LENGTH_LONG).show()
@ -276,6 +306,13 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
return@setOnClickListener
}
if (isCreateRoom && preferredGameName.isEmpty()) {
Toast.makeText(activity, R.string.multiplayer_preferred_game_name_invalid, Toast.LENGTH_LONG).show();
binding.btnConfirm.isEnabled = false
binding.btnConfirm.text = activity.getString(R.string.original_button_text)
return@setOnClickListener
}
if (ipAddress.length < 7 || username.length < 5) {
Toast.makeText(activity, R.string.multiplayer_input_invalid, Toast.LENGTH_LONG).show()
binding.btnConfirm.isEnabled = true
@ -283,7 +320,7 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
} else {
Handler(Looper.getMainLooper()).post {
val result = if (isCreateRoom) {
NetPlayManager.netPlayCreateRoom(ipAddress, port, username, password, roomName, maxPlayers)
NetPlayManager.netPlayCreateRoom(ipAddress, port, username, preferredGameName, preferredGameId, password, roomName, maxPlayers)
} else {
NetPlayManager.netPlayJoinRoom(ipAddress, port, username, password)
}
@ -397,4 +434,4 @@ class NetPlayDialog(context: Context) : BottomSheetDialog(context) {
}
}
}
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

View file

@ -7,7 +7,9 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
enum class StringSetting(override val key: String) : AbstractStringSetting {
DRIVER_PATH("driver_path"),
DEVICE_NAME("device_name");
DEVICE_NAME("device_name"),
WEB_TOKEN("yuzu_token"),
;
override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)

View file

@ -274,6 +274,13 @@ abstract class SettingsItem(
descriptionId = R.string.use_custom_rtc_description
)
)
put(
StringInputSetting(
StringSetting.WEB_TOKEN,
titleId = R.string.web_token,
descriptionId = R.string.web_token_description
)
)
put(DateTimeSetting(LongSetting.CUSTOM_RTC, titleId = R.string.set_custom_rtc))
put(
SingleChoiceSetting(

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
@ -200,6 +200,10 @@ class SettingsFragmentPresenter(
add(IntSetting.LANGUAGE_INDEX.key)
add(BooleanSetting.USE_CUSTOM_RTC.key)
add(LongSetting.CUSTOM_RTC.key)
// TODO(alekpop): Add functionality
// add(HeaderSetting(R.string.network))
// add(StringSetting.WEB_TOKEN.key)
}
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments

View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments

View file

@ -1,10 +1,6 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.Manifest
@ -128,7 +124,7 @@ class HomeSettingsFragment : Fragment() {
R.string.multiplayer_description,
R.drawable.ic_two_users,
{
val action = mainActivity.displayMultiplayerDialog()
mainActivity.displayMultiplayerDialog()
},
)
)

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model

View file

@ -1,16 +1,14 @@
// Copyright 2024 Mandarine Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.network
import android.app.Activity
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.text.format.Formatter
@ -19,10 +17,27 @@ import androidx.preference.PreferenceManager
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.dialogs.ChatMessage
import java.net.Inet4Address
object NetPlayManager {
external fun netPlayCreateRoom(ipAddress: String, port: Int, username: String, password: String, roomName: String, maxPlayers: Int): Int
external fun netPlayJoinRoom(ipAddress: String, port: Int, username: String, password: String): Int
external fun netPlayCreateRoom(
ipAddress: String,
port: Int,
username: String,
preferredGameName: String,
preferredGameId: Long,
password: String,
roomName: String,
maxPlayers: Int
): Int
external fun netPlayJoinRoom(
ipAddress: String,
port: Int,
username: String,
password: String
): Int
external fun netPlayRoomInfo(): Array<String>
external fun netPlayIsJoined(): Boolean
external fun netPlayIsHostedRoom(): Boolean
@ -33,6 +48,28 @@ object NetPlayManager {
external fun netPlayGetBanList(): Array<String>
external fun netPlayBanUser(username: String)
external fun netPlayUnbanUser(username: String)
external fun netPlayGetPublicRooms(): Array<String>
data class RoomInfo(
val name: String,
val hasPassword: Boolean,
val maxPlayers: Int,
val ip: String,
val port: Int,
val description: String,
val owner: String,
val preferredGameId: Long,
val preferredGameName: String,
val members: MutableList<RoomMember> = mutableListOf()
)
data class RoomMember(
val username: String,
val nickname: String,
val gameId: Long,
val gameName: String
)
private var messageListener: ((Int, String) -> Unit)? = null
private var adapterRefreshListener: ((Int, String) -> Unit)? = null
@ -41,11 +78,57 @@ object NetPlayManager {
messageListener = listener
}
fun getPublicRooms(): List<RoomInfo> {
val roomData = netPlayGetPublicRooms()
val rooms = mutableMapOf<String, RoomInfo>()
for (data in roomData) {
val parts = data.split("|")
if (parts[0] == "MEMBER" && parts.size >= 6) {
val roomName = parts[1]
val member = RoomMember(
username = parts[2],
nickname = parts[3],
gameId = parts[4].toLongOrNull() ?: 0L,
gameName = parts[5]
)
rooms[roomName]?.members?.add(member)
} else if (parts.size >= 9) {
val roomInfo = RoomInfo(
name = parts[0],
hasPassword = parts[1] == "1",
maxPlayers = parts[2].toIntOrNull() ?: 0,
ip = parts[3],
port = parts[4].toIntOrNull() ?: 0,
description = parts[5],
owner = parts[6],
preferredGameId = parts[7].toLongOrNull() ?: 0L,
preferredGameName = parts[8]
)
rooms[roomInfo.name] = roomInfo
}
}
return rooms.values.toList()
}
fun refreshRoomListAsync(callback: (List<RoomInfo>) -> Unit) {
Thread {
val rooms = getPublicRooms()
Handler(Looper.getMainLooper()).post {
callback(rooms)
}
}.start()
}
fun setOnAdapterRefreshListener(listener: (Int, String) -> Unit) {
adapterRefreshListener = listener
}
fun getUsername(activity: Context): String { val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
fun getUsername(activity: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(activity)
val name = "Eden${(Math.random() * 100).toInt()}"
return prefs.getString("NetPlayUsername", name) ?: name
}
@ -103,31 +186,36 @@ object NetPlayManager {
if (parts.size == 2) {
val nickname = parts[0].trim()
val chatMessage = parts[1].trim()
addChatMessage(ChatMessage(
nickname = nickname,
username = "",
message = chatMessage
))
addChatMessage(
ChatMessage(
nickname = nickname,
username = "",
message = chatMessage
)
)
}
}
NetPlayStatus.MEMBER_JOIN,
NetPlayStatus.MEMBER_LEAVE,
NetPlayStatus.MEMBER_KICKED,
NetPlayStatus.MEMBER_BANNED -> {
addChatMessage(ChatMessage(
nickname = "System",
username = "",
message = message
))
addChatMessage(
ChatMessage(
nickname = "System",
username = "",
message = message
)
)
}
}
Handler(Looper.getMainLooper()).post {
if (!isChatOpen) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
Handler(Looper.getMainLooper()).post {
if (!isChatOpen) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
messageListener?.invoke(type, msg)
@ -159,34 +247,59 @@ object NetPlayManager {
NetPlayStatus.ROOM_MODERATOR -> context.getString(R.string.multiplayer_room_moderator)
NetPlayStatus.MEMBER_JOIN -> context.getString(R.string.multiplayer_member_join, msg)
NetPlayStatus.MEMBER_LEAVE -> context.getString(R.string.multiplayer_member_leave, msg)
NetPlayStatus.MEMBER_KICKED -> context.getString(R.string.multiplayer_member_kicked, msg)
NetPlayStatus.MEMBER_BANNED -> context.getString(R.string.multiplayer_member_banned, msg)
NetPlayStatus.MEMBER_KICKED -> context.getString(
R.string.multiplayer_member_kicked,
msg
)
NetPlayStatus.MEMBER_BANNED -> context.getString(
R.string.multiplayer_member_banned,
msg
)
NetPlayStatus.ADDRESS_UNBANNED -> context.getString(R.string.multiplayer_address_unbanned)
NetPlayStatus.CHAT_MESSAGE -> msg
else -> ""
}
}
fun getIpAddressByWifi(activity: Activity): String {
var ipAddress = 0
val wifiManager = activity.getSystemService(WifiManager::class.java)
val wifiInfo = wifiManager.connectionInfo
if (wifiInfo != null) {
ipAddress = wifiInfo.ipAddress
}
fun isConnectedToWifi(activity: Activity): Boolean {
val connectivityManager = activity.getSystemService(ConnectivityManager::class.java)
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
return capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
}
if (ipAddress == 0) {
val dhcpInfo = wifiManager.dhcpInfo
if (dhcpInfo != null) {
ipAddress = dhcpInfo.ipAddress
fun getIpAddressByWifi(activity: Activity): String {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// For Android 12 (API 31) and above
val connectivityManager = activity.getSystemService(ConnectivityManager::class.java)
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
if (capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true) {
val linkProperties = connectivityManager.getLinkProperties(network)
linkProperties?.linkAddresses?.firstOrNull { it.address is Inet4Address }?.let {
return it.address.hostAddress ?: "192.168.0.1"
}
}
}
return if (ipAddress == 0) {
"192.168.0.1"
} else {
Formatter.formatIpAddress(ipAddress)
// For Android 11 (API 30) and below
try {
val connectivityManager = activity.getSystemService(ConnectivityManager::class.java)
val network = connectivityManager.activeNetwork
if (network != null) {
val linkProperties = connectivityManager.getLinkProperties(network)
linkProperties?.linkAddresses?.firstOrNull { it.address is Inet4Address }?.let {
return it.address.hostAddress ?: "192.168.0.1"
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return "192.168.0.1"
}
fun getBanList(): List<String> {
@ -223,4 +336,4 @@ object NetPlayManager {
const val ADDRESS_UNBANNED = 26
const val CHAT_MESSAGE = 27
}
}
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.ui

View file

@ -1,10 +1,6 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.ui.main
import android.content.Intent
@ -72,9 +68,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
ThemeHelper.ThemeChangeListener(this)
ThemeHelper.setTheme(this)
NativeLibrary.netPlayInit()
super.onCreate(savedInstanceState)
NativeLibrary.initMultiplayer()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

View file

@ -1,8 +1,4 @@
// Copyright 2024 Mandarine Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils

View file

@ -1,6 +1,9 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
import android.content.SharedPreferences
@ -13,11 +16,15 @@ import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
import androidx.core.content.edit
import androidx.core.net.toUri
object GameHelper {
private const val KEY_OLD_GAME_PATH = "game_path"
const val KEY_GAMES = "Games"
var cachedGameList = mutableListOf<Game>()
private lateinit var preferences: SharedPreferences
fun getGames(): List<Game> {
@ -29,7 +36,7 @@ object GameHelper {
val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: ""
if (oldGamesDir.isNotEmpty()) {
gameDirs.add(GameDir(oldGamesDir, true))
preferences.edit().remove(KEY_OLD_GAME_PATH).apply()
preferences.edit() { remove(KEY_OLD_GAME_PATH) }
}
gameDirs.addAll(NativeConfig.getGameDirs())
@ -44,7 +51,7 @@ object GameHelper {
val badDirs = mutableListOf<Int>()
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
val gameDirUri = Uri.parse(gameDir.uriString)
val gameDirUri = gameDir.uriString.toUri()
val isValid = FileUtil.isTreeUriValid(gameDirUri)
if (isValid) {
addGamesRecursive(
@ -72,11 +79,12 @@ object GameHelper {
games.forEach {
serializedGames.add(Json.encodeToString(it))
}
preferences.edit()
.remove(KEY_GAMES)
.putStringSet(KEY_GAMES, serializedGames)
.apply()
preferences.edit() {
remove(KEY_GAMES)
.putStringSet(KEY_GAMES, serializedGames)
}
cachedGameList = games.toMutableList()
return games.toList()
}

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils

View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
@ -68,12 +68,17 @@
#include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_surface.h"
#include "video_core/shader_notify.h"
#include "network/announce_multiplayer_session.h"
#define jconst [[maybe_unused]] const auto
#define jauto [[maybe_unused]] auto
static EmulationSession s_instance;
//Abdroid Multiplayer which can be initialized with parameters
std::unique_ptr<AndroidMultiplayer> multiplayer{nullptr};
std::shared_ptr<Core::AnnounceMultiplayerSession> announce_multiplayer_session;
EmulationSession::EmulationSession() {
m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
}
@ -916,12 +921,33 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, jobje
return ContentManager::AreKeysPresent();
}
JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_NativeLibrary_initMultiplayer(
JNIEnv* env, [[maybe_unused]] jobject obj) {
if (multiplayer) {
return;
}
announce_multiplayer_session = std::make_shared<Core::AnnounceMultiplayerSession>();
multiplayer = std::make_unique<AndroidMultiplayer>(s_instance.System(), announce_multiplayer_session);
multiplayer->NetworkInit();
}
JNIEXPORT jobjectArray JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayGetPublicRooms(
JNIEnv *env, [[maybe_unused]] jobject obj) {
return Common::Android::ToJStringArray(env, multiplayer->NetPlayGetPublicRooms());
}
JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayCreateRoom(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port,
jstring username, jstring password, jstring room_name, jint max_players) {
jstring username, jstring preferredGameName, jlong preferredGameId, jstring password,
jstring room_name, jint max_players) {
return static_cast<jint>(
NetPlayCreateRoom(Common::Android::GetJString(env, ipaddress), port,
Common::Android::GetJString(env, username), Common::Android::GetJString(env, password),
multiplayer->NetPlayCreateRoom(Common::Android::GetJString(env, ipaddress), port,
Common::Android::GetJString(env, username), Common::Android::GetJString(env, preferredGameName),
preferredGameId,Common::Android::GetJString(env, password),
Common::Android::GetJString(env, room_name), max_players));
}
@ -929,70 +955,63 @@ JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayJoi
JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port,
jstring username, jstring password) {
return static_cast<jint>(
NetPlayJoinRoom(Common::Android::GetJString(env, ipaddress), port,
multiplayer->NetPlayJoinRoom(Common::Android::GetJString(env, ipaddress), port,
Common::Android::GetJString(env, username), Common::Android::GetJString(env, password)));
}
JNIEXPORT jobjectArray JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayRoomInfo(
JNIEnv* env, [[maybe_unused]] jobject obj) {
return Common::Android::ToJStringArray(env, NetPlayRoomInfo());
return Common::Android::ToJStringArray(env, multiplayer->NetPlayRoomInfo());
}
JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsJoined(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
return NetPlayIsJoined();
return multiplayer->NetPlayIsJoined();
}
JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsHostedRoom(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
return NetPlayIsHostedRoom();
return multiplayer->NetPlayIsHostedRoom();
}
JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlaySendMessage(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring msg) {
NetPlaySendMessage(Common::Android::GetJString(env, msg));
multiplayer->NetPlaySendMessage(Common::Android::GetJString(env, msg));
}
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayKickUser(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
NetPlayKickUser(Common::Android::GetJString(env, username));
multiplayer->NetPlayKickUser(Common::Android::GetJString(env, username));
}
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayLeaveRoom(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
NetPlayLeaveRoom();
multiplayer->NetPlayLeaveRoom();
}
JNIEXPORT jboolean JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayIsModerator(
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
return NetPlayIsModerator();
return multiplayer->NetPlayIsModerator();
}
JNIEXPORT jobjectArray JNICALL
Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayGetBanList(
JNIEnv* env, [[maybe_unused]] jobject obj) {
return Common::Android::ToJStringArray(env, NetPlayGetBanList());
return Common::Android::ToJStringArray(env, multiplayer->NetPlayGetBanList());
}
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayBanUser(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
NetPlayBanUser(Common::Android::GetJString(env, username));
multiplayer->NetPlayBanUser(Common::Android::GetJString(env, username));
}
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_network_NetPlayManager_netPlayUnbanUser(
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
NetPlayUnbanUser(Common::Android::GetJString(env, username));
multiplayer->NetPlayUnbanUser(Common::Android::GetJString(env, username));
}
JNIEXPORT void JNICALL
Java_org_yuzu_yuzu_1emu_NativeLibrary_netPlayInit(
JNIEnv* env, [[maybe_unused]] jobject obj) {
NetworkInit(&EmulationSession::GetInstance().System().GetRoomNetwork());
}
} // extern "C"

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
</vector>

View file

@ -0,0 +1,220 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:elevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_scrollFlags="scroll|enterAlways|snap">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="16dp"
android:paddingHorizontal="16dp"
android:gravity="center_vertical">
<Space
android:layout_width="48dp"
android:layout_height="0dp" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/multiplayer_room_browser"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge" />
<FrameLayout
android:layout_width="48dp"
android:layout_height="48dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/refresh_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:text="@string/refresh"
app:icon="@drawable/ic_refresh" />
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleSmall"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<com.google.android.material.card.MaterialCardView
android:id="@+id/search_background"
style="?attr/materialCardViewFilledStyle"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
app:cardCornerRadius="24dp">
<LinearLayout
android:id="@+id/search_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="48dp"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_search"
app:tint="?attr/colorOnSurfaceVariant" />
<EditText
android:id="@+id/search_text"
android:layout_width="180dp"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:hint="@string/multiplayer_search_public_lobbies"
android:imeOptions="flagNoFullscreen"
android:inputType="text"
android:maxLines="1" />
</LinearLayout>
<ImageView
android:id="@+id/clear_button"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical|end"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackground"
android:src="@drawable/ic_clear"
android:visibility="invisible"
app:tint="?attr/colorOnSurfaceVariant"
tools:visibility="visible" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_submit"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="110dp"
android:layout_height="48dp"
android:layout_marginTop="12dp"
android:text="@string/submit" />
</LinearLayout>
<HorizontalScrollView
android:id="@+id/horizontalScrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fadingEdge="horizontal"
android:scrollbars="none"
android:layout_marginVertical="8dp"
android:clipToPadding="false"
android:paddingHorizontal="16dp">
<LinearLayout
android:id="@+id/chips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false">
<com.google.android.material.chip.Chip
android:id="@+id/chip_hide_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="true"
android:checked="false"
android:text="@string/multiplayer_hide_empty_rooms"
app:chipCornerRadius="16dp" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_hide_full"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="true"
android:checked="false"
android:text="@string/multiplayer_hide_full_rooms"
app:chipCornerRadius="16dp" />
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/room_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:contentDescription="@string/room_list"
android:paddingBottom="16dp"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<LinearLayout
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:gravity="center"
android:padding="32dp"
android:visibility="gone">
<ImageView
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_refresh"
android:alpha="0.5"
app:tint="?attr/colorOnSurface" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/multiplayer_no_rooms_found"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/multiplayer_tap_refresh_to_check_again"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
<com.google.android.material.button.MaterialButton
android:id="@+id/empty_refresh_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/refresh"
app:icon="@drawable/ic_refresh" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -28,7 +28,7 @@
<ImageView
android:layout_width="140dp"
android:layout_height="140dp"
android:layout_height="114dp"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:layout_marginBottom="24dp"
@ -38,19 +38,45 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:orientation="horizontal">
<Space
android:layout_width="20dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_lobby_browser"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="175dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/multiplayer_public_room"
app:cornerRadius="16dp"
app:icon="@drawable/ic_search" />
<Space
android:layout_width="20dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="8dp">
android:layout_marginBottom="8dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_join"
style="@style/Widget.Material3.Button.TonalButton"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/multiplayer_join_room"
app:icon="@drawable/ic_install"
app:cornerRadius="16dp" />
app:cornerRadius="16dp"
app:icon="@drawable/ic_install" />
<Space
android:layout_width="16dp"
@ -58,13 +84,13 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_create"
style="@style/Widget.Material3.Button"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/multiplayer_create_room"
app:icon="@drawable/ic_add"
app:cornerRadius="16dp" />
app:cornerRadius="16dp"
app:icon="@drawable/ic_add" />
</LinearLayout>

View file

@ -59,6 +59,21 @@
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:id="@+id/preferred_game_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:hint="@string/multiplayer_preferred_game_name">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_preferred_game_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/multiplayer_password"
android:layout_marginTop="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dropdown_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceSubtitle1" />

View file

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="?attr/materialCardViewFilledStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:layout_marginHorizontal="12dp"
android:background="?attr/selectableItemBackground"
android:backgroundTint="?attr/colorSurfaceVariant"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<ImageView
android:id="@+id/lock_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@string/multiplayer_password_protected"
android:src="@drawable/ic_lock"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnSurface" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginEnd="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@id/lock_icon"
app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/room_name"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="viewStart"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Room Name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/room_owner"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textAlignment="viewStart"
android:textSize="14sp"
tools:text="Hosted by: Owner" />
<LinearLayout
android:id="@+id/game_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="horizontal">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="center_vertical"
android:contentDescription="@string/multiplayer_game"
android:src="@drawable/ic_controller" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/game_name"
style="@style/TextAppearance.Material3.LabelMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:ellipsize="end"
android:singleLine="true"
android:textStyle="bold"
tools:text="Game Name" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/ic_user"
android:contentDescription="@string/multiplayer_player_count"
app:tint="?attr/colorAccent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/player_count"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:textColor="?attr/colorAccent"
tools:text="2/4" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Eden</string>
</resources>

View file

@ -399,6 +399,11 @@
<string name="use_custom_rtc_description">Allows you to set a custom real-time clock separate from your current system time.</string>
<string name="set_custom_rtc">Set custom RTC</string>
<!-- Network settings strings -->
<string name="web_token">Web Token</string>
<string name="web_token_description">Web token used for creating public lobbies. It is a 48-character string containing only lowercase a-z.</string>
<string name="network">Network</string>
<!-- Graphics settings strings -->
<string name="frame_skipping">WIP: Frameskip</string>
<string name="frame_skipping_description">Toggle frame skipping to improve performance by reducing the number of rendered frames. This feature is still being worked on and will be enabled in future releases.</string>