Browse Source
android: Add initial frontend for LAN network rooms (#76)
android: Add initial frontend for LAN network rooms (#76)
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/76 Co-authored-by: Briar <205427297+icy-briar@users.noreply.github.com> Co-committed-by: Briar <205427297+icy-briar@users.noreply.github.com>remotes/1778439668340191200/nce_cpp
committed by
icy-briar
44 changed files with 2105 additions and 19 deletions
-
1src/android/app/src/main/AndroidManifest.xml
-
137src/android/app/src/main/java/org/citron/citron_emu/dialogs/ChatDialog.kt
-
400src/android/app/src/main/java/org/citron/citron_emu/dialogs/NetPlayDialog.kt
-
226src/android/app/src/main/java/org/citron/citron_emu/network/NetPlayManager.kt
-
26src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
-
16src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
-
11src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
-
14src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
-
11src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
-
22src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/CompatUtils.kt
-
84src/android/app/src/main/jni/native.cpp
-
10src/android/app/src/main/res/drawable/ic_chat.xml
-
10src/android/app/src/main/res/drawable/ic_network.xml
-
9src/android/app/src/main/res/drawable/ic_send.xml
-
10src/android/app/src/main/res/drawable/ic_system.xml
-
10src/android/app/src/main/res/drawable/ic_two_users.xml
-
9src/android/app/src/main/res/drawable/ic_user.xml
-
7src/android/app/src/main/res/layout/dialog_ban_list.xml
-
38src/android/app/src/main/res/layout/dialog_bottom_sheet.xml
-
55src/android/app/src/main/res/layout/dialog_chat.xml
-
72src/android/app/src/main/res/layout/dialog_multiplayer_connect.xml
-
75src/android/app/src/main/res/layout/dialog_multiplayer_lobby.xml
-
119src/android/app/src/main/res/layout/dialog_multiplayer_room.xml
-
28src/android/app/src/main/res/layout/item_ban_list.xml
-
25src/android/app/src/main/res/layout/item_button_netplay.xml
-
36src/android/app/src/main/res/layout/item_chat_message.xml
-
5src/android/app/src/main/res/layout/item_netplay_separator.xml
-
18src/android/app/src/main/res/layout/item_netplay_text.xml
-
7src/android/app/src/main/res/layout/item_separator_netplay.xml
-
24src/android/app/src/main/res/layout/item_text_netplay.xml
-
10src/android/app/src/main/res/menu/menu_in_game.xml
-
13src/android/app/src/main/res/menu/menu_netplay_member.xml
-
72src/android/app/src/main/res/values/strings.xml
-
2src/common/CMakeLists.txt
-
12src/common/android/android_common.cpp
-
5src/common/android/android_common.h
-
24src/common/android/id_cache.cpp
-
7src/common/android/id_cache.h
-
353src/common/android/multiplayer/multiplayer.cpp
-
68src/common/android/multiplayer/multiplayer.h
-
4src/common/announce_multiplayer_room.h
-
14src/dedicated_room/yuzu_room.cpp
-
19src/network/room.cpp
-
6src/network/room.h
@ -0,0 +1,137 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright yuzu/Citra Emulator Project / Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
|
||||
|
package org.yuzu.yuzu_emu.dialogs |
||||
|
|
||||
|
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.ViewGroup |
||||
|
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 org.yuzu.yuzu_emu.R |
||||
|
import org.yuzu.yuzu_emu.databinding.DialogChatBinding |
||||
|
import org.yuzu.yuzu_emu.databinding.ItemChatMessageBinding |
||||
|
import org.yuzu.yuzu_emu.network.NetPlayManager |
||||
|
import java.text.SimpleDateFormat |
||||
|
import java.util.* |
||||
|
|
||||
|
class ChatMessage( |
||||
|
val nickname: String, // This is the common name youll see on private servers |
||||
|
val username: String, // Username is the community/forum username |
||||
|
val message: String, |
||||
|
val timestamp: String = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date()) |
||||
|
) { |
||||
|
} |
||||
|
|
||||
|
class ChatDialog(context: Context) : BottomSheetDialog(context) { |
||||
|
private lateinit var binding: DialogChatBinding |
||||
|
private lateinit var chatAdapter: ChatAdapter |
||||
|
private val handler = Handler(Looper.getMainLooper()) |
||||
|
|
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
super.onCreate(savedInstanceState) |
||||
|
binding = DialogChatBinding.inflate(LayoutInflater.from(context)) |
||||
|
setContentView(binding.root) |
||||
|
|
||||
|
NetPlayManager.setChatOpen(true) |
||||
|
setupRecyclerView() |
||||
|
|
||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED |
||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED |
||||
|
behavior.skipCollapsed = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE |
||||
|
|
||||
|
|
||||
|
handler.post { |
||||
|
chatAdapter.notifyDataSetChanged() |
||||
|
binding.chatRecyclerView.post { |
||||
|
scrollToBottom() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
NetPlayManager.setOnMessageReceivedListener { type, message -> |
||||
|
handler.post { |
||||
|
chatAdapter.notifyDataSetChanged() |
||||
|
scrollToBottom() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
binding.sendButton.setOnClickListener { |
||||
|
val message = binding.chatInput.text.toString() |
||||
|
if (message.isNotBlank()) { |
||||
|
sendMessage(message) |
||||
|
binding.chatInput.text?.clear() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun dismiss() { |
||||
|
NetPlayManager.setChatOpen(false) |
||||
|
super.dismiss() |
||||
|
} |
||||
|
|
||||
|
private fun sendMessage(message: String) { |
||||
|
val username = NetPlayManager.getUsername(context) |
||||
|
NetPlayManager.netPlaySendMessage(message) |
||||
|
|
||||
|
val chatMessage = ChatMessage( |
||||
|
nickname = username, |
||||
|
username = "", |
||||
|
message = message, |
||||
|
timestamp = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date()) |
||||
|
) |
||||
|
|
||||
|
NetPlayManager.addChatMessage(chatMessage) |
||||
|
chatAdapter.notifyDataSetChanged() |
||||
|
scrollToBottom() |
||||
|
} |
||||
|
|
||||
|
private fun setupRecyclerView() { |
||||
|
chatAdapter = ChatAdapter(NetPlayManager.getChatMessages()) |
||||
|
binding.chatRecyclerView.layoutManager = LinearLayoutManager(context).apply { |
||||
|
stackFromEnd = true |
||||
|
} |
||||
|
binding.chatRecyclerView.adapter = chatAdapter |
||||
|
} |
||||
|
|
||||
|
private fun scrollToBottom() { |
||||
|
binding.chatRecyclerView.scrollToPosition(chatAdapter.itemCount - 1) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class ChatAdapter(private val messages: List<ChatMessage>) : |
||||
|
RecyclerView.Adapter<ChatAdapter.ChatViewHolder>() { |
||||
|
|
||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatViewHolder { |
||||
|
val binding = ItemChatMessageBinding.inflate( |
||||
|
LayoutInflater.from(parent.context), |
||||
|
parent, |
||||
|
false |
||||
|
) |
||||
|
return ChatViewHolder(binding) |
||||
|
} |
||||
|
|
||||
|
override fun getItemCount(): Int = messages.size |
||||
|
|
||||
|
override fun onBindViewHolder(holder: ChatViewHolder, position: Int) { |
||||
|
holder.bind(messages[position]) |
||||
|
} |
||||
|
|
||||
|
inner class ChatViewHolder(private val binding: ItemChatMessageBinding) : |
||||
|
RecyclerView.ViewHolder(binding.root) { |
||||
|
fun bind(message: ChatMessage) { |
||||
|
binding.usernameText.text = message.nickname |
||||
|
binding.messageText.text = message.message |
||||
|
binding.userIcon.setImageResource(when (message.nickname) { |
||||
|
"System" -> R.drawable.ic_system |
||||
|
else -> R.drawable.ic_user |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,400 @@ |
|||||
|
// 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-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.dialogs |
||||
|
|
||||
|
import android.content.Context |
||||
|
import org.yuzu.yuzu_emu.R |
||||
|
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.widget.PopupMenu |
||||
|
import android.widget.Toast |
||||
|
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 org.yuzu.yuzu_emu.YuzuApplication |
||||
|
import org.yuzu.yuzu_emu.databinding.DialogMultiplayerConnectBinding |
||||
|
import org.yuzu.yuzu_emu.databinding.DialogMultiplayerLobbyBinding |
||||
|
import org.yuzu.yuzu_emu.databinding.DialogMultiplayerRoomBinding |
||||
|
import org.yuzu.yuzu_emu.databinding.ItemBanListBinding |
||||
|
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 |
||||
|
|
||||
|
class NetPlayDialog(context: Context) : BottomSheetDialog(context) { |
||||
|
private lateinit var adapter: NetPlayAdapter |
||||
|
|
||||
|
|
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
super.onCreate(savedInstanceState) |
||||
|
|
||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED |
||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED |
||||
|
behavior.skipCollapsed = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE |
||||
|
|
||||
|
when { |
||||
|
NetPlayManager.netPlayIsJoined() -> DialogMultiplayerLobbyBinding.inflate(layoutInflater) |
||||
|
.apply { |
||||
|
setContentView(root) |
||||
|
adapter = NetPlayAdapter() |
||||
|
listMultiplayer.layoutManager = LinearLayoutManager(context) |
||||
|
listMultiplayer.adapter = adapter |
||||
|
adapter.loadMultiplayerMenu() |
||||
|
btnLeave.setOnClickListener { |
||||
|
NetPlayManager.netPlayLeaveRoom() |
||||
|
dismiss() |
||||
|
} |
||||
|
btnChat.setOnClickListener { |
||||
|
ChatDialog(context).show() |
||||
|
} |
||||
|
|
||||
|
refreshAdapterItems() |
||||
|
|
||||
|
btnModeration.visibility = if (NetPlayManager.netPlayIsModerator()) View.VISIBLE else View.GONE |
||||
|
btnModeration.setOnClickListener { |
||||
|
showModerationDialog() |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
else -> { |
||||
|
DialogMultiplayerConnectBinding.inflate(layoutInflater).apply { |
||||
|
setContentView(root) |
||||
|
btnCreate.setOnClickListener { |
||||
|
showNetPlayInputDialog(true) |
||||
|
dismiss() |
||||
|
} |
||||
|
btnJoin.setOnClickListener { |
||||
|
showNetPlayInputDialog(false) |
||||
|
dismiss() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
data class NetPlayItems( |
||||
|
val option: Int, |
||||
|
val name: String, |
||||
|
val type: Int, |
||||
|
val id: Int = 0 |
||||
|
) { |
||||
|
companion object { |
||||
|
const val MULTIPLAYER_ROOM_TEXT = 1 |
||||
|
const val MULTIPLAYER_ROOM_MEMBER = 2 |
||||
|
const val MULTIPLAYER_SEPARATOR = 3 |
||||
|
const val MULTIPLAYER_ROOM_COUNT = 4 |
||||
|
const val TYPE_BUTTON = 0 |
||||
|
const val TYPE_TEXT = 1 |
||||
|
const val TYPE_SEPARATOR = 2 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
inner class NetPlayAdapter : RecyclerView.Adapter<NetPlayAdapter.NetPlayViewHolder>() { |
||||
|
val netPlayItems = mutableListOf<NetPlayItems>() |
||||
|
|
||||
|
abstract inner class NetPlayViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener { |
||||
|
init { |
||||
|
itemView.setOnClickListener(this) |
||||
|
} |
||||
|
abstract fun bind(item: NetPlayItems) |
||||
|
} |
||||
|
|
||||
|
inner class TextViewHolder(private val binding: ItemTextNetplayBinding) : NetPlayViewHolder(binding.root) { |
||||
|
private lateinit var netPlayItem: NetPlayItems |
||||
|
|
||||
|
override fun onClick(clicked: View) {} |
||||
|
|
||||
|
override fun bind(item: NetPlayItems) { |
||||
|
netPlayItem = item |
||||
|
binding.itemTextNetplayName.text = item.name |
||||
|
binding.itemIcon.apply { |
||||
|
val iconRes = when (item.option) { |
||||
|
NetPlayItems.MULTIPLAYER_ROOM_TEXT -> R.drawable.ic_system |
||||
|
NetPlayItems.MULTIPLAYER_ROOM_COUNT -> R.drawable.ic_two_users |
||||
|
else -> 0 |
||||
|
} |
||||
|
visibility = if (iconRes != 0) { |
||||
|
setImageResource(iconRes) |
||||
|
View.VISIBLE |
||||
|
} else View.GONE |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
inner class ButtonViewHolder(private val binding: ItemButtonNetplayBinding) : NetPlayViewHolder(binding.root) { |
||||
|
private lateinit var netPlayItems: NetPlayItems |
||||
|
private val isModerator = NetPlayManager.netPlayIsModerator() |
||||
|
|
||||
|
init { |
||||
|
binding.itemButtonMore.apply { |
||||
|
visibility = View.VISIBLE |
||||
|
setOnClickListener { showPopupMenu(it) } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun onClick(clicked: View) {} |
||||
|
|
||||
|
|
||||
|
private fun showPopupMenu(view: View) { |
||||
|
PopupMenu(view.context, view).apply { |
||||
|
menuInflater.inflate(R.menu.menu_netplay_member, menu) |
||||
|
menu.findItem(R.id.action_kick).isEnabled = isModerator && |
||||
|
netPlayItems.name != NetPlayManager.getUsername(context) |
||||
|
menu.findItem(R.id.action_ban).isEnabled = isModerator && |
||||
|
netPlayItems.name != NetPlayManager.getUsername(context) |
||||
|
setOnMenuItemClickListener { item -> |
||||
|
if (item.itemId == R.id.action_kick) { |
||||
|
NetPlayManager.netPlayKickUser(netPlayItems.name) |
||||
|
true |
||||
|
} else if (item.itemId == R.id.action_ban) { |
||||
|
NetPlayManager.netPlayBanUser(netPlayItems.name) |
||||
|
true |
||||
|
} else false |
||||
|
} |
||||
|
show() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun bind(item: NetPlayItems) { |
||||
|
netPlayItems = item |
||||
|
binding.itemButtonNetplayName.text = netPlayItems.name |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fun loadMultiplayerMenu() { |
||||
|
val infos = NetPlayManager.netPlayRoomInfo() |
||||
|
if (infos.isNotEmpty()) { |
||||
|
val roomInfo = infos[0].split("|") |
||||
|
netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_ROOM_TEXT, roomInfo[0], NetPlayItems.TYPE_TEXT)) |
||||
|
netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_ROOM_COUNT, "${infos.size - 1}/${roomInfo[1]}", NetPlayItems.TYPE_TEXT)) |
||||
|
netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_SEPARATOR, "", NetPlayItems.TYPE_SEPARATOR)) |
||||
|
for (i in 1 until infos.size) { |
||||
|
netPlayItems.add(NetPlayItems(NetPlayItems.MULTIPLAYER_ROOM_MEMBER, infos[i], NetPlayItems.TYPE_BUTTON)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun getItemViewType(position: Int) = netPlayItems[position].type |
||||
|
|
||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NetPlayViewHolder { |
||||
|
val inflater = LayoutInflater.from(parent.context) |
||||
|
return when (viewType) { |
||||
|
NetPlayItems.TYPE_TEXT -> TextViewHolder(ItemTextNetplayBinding.inflate(inflater, parent, false)) |
||||
|
NetPlayItems.TYPE_BUTTON -> ButtonViewHolder(ItemButtonNetplayBinding.inflate(inflater, parent, false)) |
||||
|
NetPlayItems.TYPE_SEPARATOR -> object : NetPlayViewHolder(inflater.inflate(R.layout.item_separator_netplay, parent, false)) { |
||||
|
override fun bind(item: NetPlayItems) {} |
||||
|
override fun onClick(clicked: View) {} |
||||
|
} |
||||
|
else -> throw IllegalStateException("Unsupported view type") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun onBindViewHolder(holder: NetPlayViewHolder, position: Int) { |
||||
|
holder.bind(netPlayItems[position]) |
||||
|
} |
||||
|
|
||||
|
override fun getItemCount() = netPlayItems.size |
||||
|
} |
||||
|
|
||||
|
fun refreshAdapterItems() { |
||||
|
val handler = Handler(Looper.getMainLooper()) |
||||
|
|
||||
|
NetPlayManager.setOnAdapterRefreshListener() { type, msg -> |
||||
|
handler.post { |
||||
|
adapter.netPlayItems.clear() |
||||
|
adapter.loadMultiplayerMenu() |
||||
|
adapter.notifyDataSetChanged() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun showNetPlayInputDialog(isCreateRoom: Boolean) { |
||||
|
val activity = CompatUtils.findActivity(context) |
||||
|
val dialog = BottomSheetDialog(activity) |
||||
|
|
||||
|
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED |
||||
|
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED |
||||
|
dialog.behavior.skipCollapsed = context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE |
||||
|
|
||||
|
|
||||
|
val binding = DialogMultiplayerRoomBinding.inflate(LayoutInflater.from(activity)) |
||||
|
dialog.setContentView(binding.root) |
||||
|
|
||||
|
binding.textTitle.text = activity.getString( |
||||
|
if (isCreateRoom) R.string.multiplayer_create_room |
||||
|
else R.string.multiplayer_join_room |
||||
|
) |
||||
|
|
||||
|
binding.ipAddress.setText( |
||||
|
if (isCreateRoom) NetPlayManager.getIpAddressByWifi(activity) |
||||
|
else NetPlayManager.getRoomAddress(activity) |
||||
|
) |
||||
|
binding.ipPort.setText(NetPlayManager.getRoomPort(activity)) |
||||
|
binding.username.setText(NetPlayManager.getUsername(activity)) |
||||
|
|
||||
|
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()) |
||||
|
|
||||
|
binding.maxPlayers.addOnChangeListener { _, value, _ -> |
||||
|
binding.maxPlayersLabel.text = context.getString(R.string.multiplayer_max_players_value, value.toInt()) |
||||
|
} |
||||
|
|
||||
|
binding.btnConfirm.setOnClickListener { |
||||
|
binding.btnConfirm.isEnabled = false |
||||
|
binding.btnConfirm.text = activity.getString(R.string.disabled_button_text) |
||||
|
|
||||
|
val ipAddress = binding.ipAddress.text.toString() |
||||
|
val username = binding.username.text.toString() |
||||
|
val portStr = binding.ipPort.text.toString() |
||||
|
val password = binding.password.text.toString() |
||||
|
val port = portStr.toIntOrNull() ?: run { |
||||
|
Toast.makeText(activity, R.string.multiplayer_port_invalid, Toast.LENGTH_LONG).show() |
||||
|
binding.btnConfirm.isEnabled = true |
||||
|
binding.btnConfirm.text = activity.getString(R.string.original_button_text) |
||||
|
return@setOnClickListener |
||||
|
} |
||||
|
val roomName = binding.roomName.text.toString() |
||||
|
val maxPlayers = binding.maxPlayers.value.toInt() |
||||
|
|
||||
|
if (isCreateRoom && (roomName.length !in 3..20)) { |
||||
|
Toast.makeText(activity, R.string.multiplayer_room_name_invalid, Toast.LENGTH_LONG).show() |
||||
|
binding.btnConfirm.isEnabled = true |
||||
|
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 |
||||
|
binding.btnConfirm.text = activity.getString(R.string.original_button_text) |
||||
|
} else { |
||||
|
Handler(Looper.getMainLooper()).post { |
||||
|
val result = if (isCreateRoom) { |
||||
|
NetPlayManager.netPlayCreateRoom(ipAddress, port, username, password, roomName, maxPlayers) |
||||
|
} else { |
||||
|
NetPlayManager.netPlayJoinRoom(ipAddress, port, username, password) |
||||
|
} |
||||
|
|
||||
|
if (result == 0) { |
||||
|
NetPlayManager.setUsername(activity, username) |
||||
|
NetPlayManager.setRoomPort(activity, portStr) |
||||
|
if (!isCreateRoom) NetPlayManager.setRoomAddress(activity, ipAddress) |
||||
|
Toast.makeText( |
||||
|
YuzuApplication.appContext, |
||||
|
if (isCreateRoom) R.string.multiplayer_create_room_success |
||||
|
else R.string.multiplayer_join_room_success, |
||||
|
Toast.LENGTH_LONG |
||||
|
).show() |
||||
|
dialog.dismiss() |
||||
|
} else { |
||||
|
Toast.makeText(activity, R.string.multiplayer_could_not_connect, Toast.LENGTH_LONG).show() |
||||
|
binding.btnConfirm.isEnabled = true |
||||
|
binding.btnConfirm.text = activity.getString(R.string.original_button_text) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
dialog.show() |
||||
|
} |
||||
|
|
||||
|
private fun showModerationDialog() { |
||||
|
val activity = CompatUtils.findActivity(context) |
||||
|
val dialog = MaterialAlertDialogBuilder(activity) |
||||
|
dialog.setTitle(R.string.multiplayer_moderation_title) |
||||
|
|
||||
|
val banList = NetPlayManager.getBanList() |
||||
|
if (banList.isEmpty()) { |
||||
|
dialog.setMessage(R.string.multiplayer_no_bans) |
||||
|
dialog.setPositiveButton(R.string.ok, null) |
||||
|
dialog.show() |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_ban_list, null) |
||||
|
val recyclerView = view.findViewById<RecyclerView>(R.id.ban_list_recycler) |
||||
|
recyclerView.layoutManager = LinearLayoutManager(context) |
||||
|
|
||||
|
lateinit var adapter: BanListAdapter |
||||
|
|
||||
|
val onUnban: (String) -> Unit = { bannedItem -> |
||||
|
MaterialAlertDialogBuilder(activity) |
||||
|
.setTitle(R.string.multiplayer_unban_title) |
||||
|
.setMessage(activity.getString(R.string.multiplayer_unban_message, bannedItem)) |
||||
|
.setPositiveButton(R.string.multiplayer_unban) { _, _ -> |
||||
|
NetPlayManager.netPlayUnbanUser(bannedItem) |
||||
|
adapter.removeBan(bannedItem) |
||||
|
} |
||||
|
.setNegativeButton(R.string.cancel, null) |
||||
|
.show() |
||||
|
} |
||||
|
|
||||
|
adapter = BanListAdapter(banList, onUnban) |
||||
|
recyclerView.adapter = adapter |
||||
|
|
||||
|
dialog.setView(view) |
||||
|
dialog.setPositiveButton(R.string.ok, null) |
||||
|
dialog.show() |
||||
|
} |
||||
|
|
||||
|
private class BanListAdapter( |
||||
|
banList: List<String>, |
||||
|
private val onUnban: (String) -> Unit |
||||
|
) : RecyclerView.Adapter<BanListAdapter.ViewHolder>() { |
||||
|
|
||||
|
private val usernameBans = banList.filter { !it.contains(".") }.toMutableList() |
||||
|
private val ipBans = banList.filter { it.contains(".") }.toMutableList() |
||||
|
|
||||
|
class ViewHolder(val binding: ItemBanListBinding) : RecyclerView.ViewHolder(binding.root) |
||||
|
|
||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |
||||
|
val binding = ItemBanListBinding.inflate( |
||||
|
LayoutInflater.from(parent.context), parent, false) |
||||
|
return ViewHolder(binding) |
||||
|
} |
||||
|
|
||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) { |
||||
|
val isUsername = position < usernameBans.size |
||||
|
val item = if (isUsername) usernameBans[position] else ipBans[position - usernameBans.size] |
||||
|
|
||||
|
holder.binding.apply { |
||||
|
banText.text = item |
||||
|
icon.setImageResource(if (isUsername) R.drawable.ic_user else R.drawable.ic_system) |
||||
|
btnUnban.setOnClickListener { onUnban(item) } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun getItemCount() = usernameBans.size + ipBans.size |
||||
|
|
||||
|
fun removeBan(bannedItem: String) { |
||||
|
val position = if (bannedItem.contains(".")) { |
||||
|
ipBans.indexOf(bannedItem).let { if (it >= 0) it + usernameBans.size else it } |
||||
|
} else { |
||||
|
usernameBans.indexOf(bannedItem) |
||||
|
} |
||||
|
|
||||
|
if (position >= 0) { |
||||
|
if (bannedItem.contains(".")) { |
||||
|
ipBans.remove(bannedItem) |
||||
|
} else { |
||||
|
usernameBans.remove(bannedItem) |
||||
|
} |
||||
|
notifyItemRemoved(position) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,226 @@ |
|||||
|
// 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-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
|
||||
|
package org.yuzu.yuzu_emu.network |
||||
|
|
||||
|
import android.app.Activity |
||||
|
import android.content.Context |
||||
|
import android.net.wifi.WifiManager |
||||
|
import android.os.Handler |
||||
|
import android.os.Looper |
||||
|
import android.text.format.Formatter |
||||
|
import android.widget.Toast |
||||
|
import androidx.preference.PreferenceManager |
||||
|
import org.yuzu.yuzu_emu.YuzuApplication |
||||
|
import org.yuzu.yuzu_emu.R |
||||
|
import org.yuzu.yuzu_emu.dialogs.ChatMessage |
||||
|
|
||||
|
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 netPlayRoomInfo(): Array<String> |
||||
|
external fun netPlayIsJoined(): Boolean |
||||
|
external fun netPlayIsHostedRoom(): Boolean |
||||
|
external fun netPlaySendMessage(msg: String) |
||||
|
external fun netPlayKickUser(username: String) |
||||
|
external fun netPlayLeaveRoom() |
||||
|
external fun netPlayIsModerator(): Boolean |
||||
|
external fun netPlayGetBanList(): Array<String> |
||||
|
external fun netPlayBanUser(username: String) |
||||
|
external fun netPlayUnbanUser(username: String) |
||||
|
|
||||
|
private var messageListener: ((Int, String) -> Unit)? = null |
||||
|
private var adapterRefreshListener: ((Int, String) -> Unit)? = null |
||||
|
|
||||
|
fun setOnMessageReceivedListener(listener: (Int, String) -> Unit) { |
||||
|
messageListener = listener |
||||
|
} |
||||
|
|
||||
|
fun setOnAdapterRefreshListener(listener: (Int, String) -> Unit) { |
||||
|
adapterRefreshListener = listener |
||||
|
} |
||||
|
|
||||
|
fun getUsername(activity: Context): String { val prefs = PreferenceManager.getDefaultSharedPreferences(activity) |
||||
|
val name = "Eden${(Math.random() * 100).toInt()}" |
||||
|
return prefs.getString("NetPlayUsername", name) ?: name |
||||
|
} |
||||
|
|
||||
|
fun setUsername(activity: Activity, name: String) { |
||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(activity) |
||||
|
prefs.edit().putString("NetPlayUsername", name).apply() |
||||
|
} |
||||
|
|
||||
|
fun getRoomAddress(activity: Activity): String { |
||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(activity) |
||||
|
val address = getIpAddressByWifi(activity) |
||||
|
return prefs.getString("NetPlayRoomAddress", address) ?: address |
||||
|
} |
||||
|
|
||||
|
fun setRoomAddress(activity: Activity, address: String) { |
||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(activity) |
||||
|
prefs.edit().putString("NetPlayRoomAddress", address).apply() |
||||
|
} |
||||
|
|
||||
|
fun getRoomPort(activity: Activity): String { |
||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(activity) |
||||
|
return prefs.getString("NetPlayRoomPort", "24872") ?: "24872" |
||||
|
} |
||||
|
|
||||
|
fun setRoomPort(activity: Activity, port: String) { |
||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(activity) |
||||
|
prefs.edit().putString("NetPlayRoomPort", port).apply() |
||||
|
} |
||||
|
|
||||
|
private val chatMessages = mutableListOf<ChatMessage>() |
||||
|
private var isChatOpen = false |
||||
|
|
||||
|
fun addChatMessage(message: ChatMessage) { |
||||
|
chatMessages.add(message) |
||||
|
} |
||||
|
|
||||
|
fun getChatMessages(): List<ChatMessage> = chatMessages |
||||
|
|
||||
|
fun clearChat() { |
||||
|
chatMessages.clear() |
||||
|
} |
||||
|
|
||||
|
fun setChatOpen(isOpen: Boolean) { |
||||
|
isChatOpen = isOpen |
||||
|
} |
||||
|
|
||||
|
fun addNetPlayMessage(type: Int, msg: String) { |
||||
|
val context = YuzuApplication.appContext |
||||
|
val message = formatNetPlayStatus(context, type, msg) |
||||
|
|
||||
|
when (type) { |
||||
|
NetPlayStatus.CHAT_MESSAGE -> { |
||||
|
val parts = msg.split(":", limit = 2) |
||||
|
if (parts.size == 2) { |
||||
|
val nickname = parts[0].trim() |
||||
|
val chatMessage = parts[1].trim() |
||||
|
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 |
||||
|
)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
Handler(Looper.getMainLooper()).post { |
||||
|
if (!isChatOpen) { |
||||
|
Toast.makeText(context, message, Toast.LENGTH_SHORT).show() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
messageListener?.invoke(type, msg) |
||||
|
adapterRefreshListener?.invoke(type, msg) |
||||
|
} |
||||
|
|
||||
|
private fun formatNetPlayStatus(context: Context, type: Int, msg: String): String { |
||||
|
return when (type) { |
||||
|
NetPlayStatus.NETWORK_ERROR -> context.getString(R.string.multiplayer_network_error) |
||||
|
NetPlayStatus.LOST_CONNECTION -> context.getString(R.string.multiplayer_lost_connection) |
||||
|
NetPlayStatus.NAME_COLLISION -> context.getString(R.string.multiplayer_name_collision) |
||||
|
NetPlayStatus.MAC_COLLISION -> context.getString(R.string.multiplayer_mac_collision) |
||||
|
NetPlayStatus.CONSOLE_ID_COLLISION -> context.getString(R.string.multiplayer_console_id_collision) |
||||
|
NetPlayStatus.WRONG_VERSION -> context.getString(R.string.multiplayer_wrong_version) |
||||
|
NetPlayStatus.WRONG_PASSWORD -> context.getString(R.string.multiplayer_wrong_password) |
||||
|
NetPlayStatus.COULD_NOT_CONNECT -> context.getString(R.string.multiplayer_could_not_connect) |
||||
|
NetPlayStatus.ROOM_IS_FULL -> context.getString(R.string.multiplayer_room_is_full) |
||||
|
NetPlayStatus.HOST_BANNED -> context.getString(R.string.multiplayer_host_banned) |
||||
|
NetPlayStatus.PERMISSION_DENIED -> context.getString(R.string.multiplayer_permission_denied) |
||||
|
NetPlayStatus.NO_SUCH_USER -> context.getString(R.string.multiplayer_no_such_user) |
||||
|
NetPlayStatus.ALREADY_IN_ROOM -> context.getString(R.string.multiplayer_already_in_room) |
||||
|
NetPlayStatus.CREATE_ROOM_ERROR -> context.getString(R.string.multiplayer_create_room_error) |
||||
|
NetPlayStatus.HOST_KICKED -> context.getString(R.string.multiplayer_host_kicked) |
||||
|
NetPlayStatus.UNKNOWN_ERROR -> context.getString(R.string.multiplayer_unknown_error) |
||||
|
NetPlayStatus.ROOM_UNINITIALIZED -> context.getString(R.string.multiplayer_room_uninitialized) |
||||
|
NetPlayStatus.ROOM_IDLE -> context.getString(R.string.multiplayer_room_idle) |
||||
|
NetPlayStatus.ROOM_JOINING -> context.getString(R.string.multiplayer_room_joining) |
||||
|
NetPlayStatus.ROOM_JOINED -> context.getString(R.string.multiplayer_room_joined) |
||||
|
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.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 |
||||
|
} |
||||
|
|
||||
|
if (ipAddress == 0) { |
||||
|
val dhcpInfo = wifiManager.dhcpInfo |
||||
|
if (dhcpInfo != null) { |
||||
|
ipAddress = dhcpInfo.ipAddress |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return if (ipAddress == 0) { |
||||
|
"192.168.0.1" |
||||
|
} else { |
||||
|
Formatter.formatIpAddress(ipAddress) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
fun getBanList(): List<String> { |
||||
|
return netPlayGetBanList().toList() |
||||
|
} |
||||
|
|
||||
|
object NetPlayStatus { |
||||
|
const val NO_ERROR = 0 |
||||
|
const val NETWORK_ERROR = 1 |
||||
|
const val LOST_CONNECTION = 2 |
||||
|
const val NAME_COLLISION = 3 |
||||
|
const val MAC_COLLISION = 4 |
||||
|
const val CONSOLE_ID_COLLISION = 5 |
||||
|
const val WRONG_VERSION = 6 |
||||
|
const val WRONG_PASSWORD = 7 |
||||
|
const val COULD_NOT_CONNECT = 8 |
||||
|
const val ROOM_IS_FULL = 9 |
||||
|
const val HOST_BANNED = 10 |
||||
|
const val PERMISSION_DENIED = 11 |
||||
|
const val NO_SUCH_USER = 12 |
||||
|
const val ALREADY_IN_ROOM = 13 |
||||
|
const val CREATE_ROOM_ERROR = 14 |
||||
|
const val HOST_KICKED = 15 |
||||
|
const val UNKNOWN_ERROR = 16 |
||||
|
const val ROOM_UNINITIALIZED = 17 |
||||
|
const val ROOM_IDLE = 18 |
||||
|
const val ROOM_JOINING = 19 |
||||
|
const val ROOM_JOINED = 20 |
||||
|
const val ROOM_MODERATOR = 21 |
||||
|
const val MEMBER_JOIN = 22 |
||||
|
const val MEMBER_LEAVE = 23 |
||||
|
const val MEMBER_KICKED = 24 |
||||
|
const val MEMBER_BANNED = 25 |
||||
|
const val ADDRESS_UNBANNED = 26 |
||||
|
const val CHAT_MESSAGE = 27 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
// 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-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
package org.yuzu.yuzu_emu.utils |
||||
|
|
||||
|
import android.app.Activity |
||||
|
import android.content.Context |
||||
|
import android.content.ContextWrapper |
||||
|
|
||||
|
object CompatUtils { |
||||
|
fun findActivity(context: Context): Activity { |
||||
|
return when (context) { |
||||
|
is Activity -> context |
||||
|
is ContextWrapper -> findActivity(context.baseContext) |
||||
|
else -> throw IllegalArgumentException("Context is not an Activity") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/> |
||||
|
</vector> |
||||
@ -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="?attr/colorPrimary" |
||||
|
android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2C15.14,9.14 8.87,9.14 5,13z"/> |
||||
|
</vector> |
||||
@ -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="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/> |
||||
|
</vector> |
||||
@ -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="M21,3L3,3c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h5v2h8v-2h5c1.1,0 1.99,-0.9 1.99,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,17L3,17L3,5h18v12z"/> |
||||
|
</vector> |
||||
@ -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> |
||||
@ -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,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/> |
||||
|
</vector> |
||||
@ -0,0 +1,7 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<androidx.recyclerview.widget.RecyclerView |
||||
|
android:id="@+id/ban_list_recycler" |
||||
|
xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:padding="8dp"/> |
||||
@ -0,0 +1,38 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="match_parent" |
||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
|
android:orientation="vertical" |
||||
|
android:gravity="center" |
||||
|
app:strokeWidth="0dp" |
||||
|
app:cardCornerRadius="24dp"> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="match_parent" |
||||
|
android:orientation="vertical" |
||||
|
android:gravity="center" |
||||
|
android:background="?colorSurface"> |
||||
|
|
||||
|
<View |
||||
|
android:layout_width="128dp" |
||||
|
android:layout_height="4dp" |
||||
|
android:layout_marginVertical="8dp" |
||||
|
android:backgroundTint="?colorSurfaceVariant" /> |
||||
|
|
||||
|
<androidx.core.widget.NestedScrollView |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="match_parent"> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:id="@+id/content" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:orientation="vertical" /> |
||||
|
|
||||
|
</androidx.core.widget.NestedScrollView> |
||||
|
|
||||
|
</LinearLayout> |
||||
|
|
||||
|
</com.google.android.material.card.MaterialCardView> |
||||
@ -0,0 +1,55 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
|
android:orientation="vertical" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="match_parent" |
||||
|
android:padding="16dp"> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/text_title" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:text="@string/chat" |
||||
|
android:textAppearance="?attr/textAppearanceHeadline6" |
||||
|
android:gravity="center" |
||||
|
android:layout_marginBottom="16dp" /> |
||||
|
|
||||
|
<androidx.recyclerview.widget.RecyclerView |
||||
|
android:id="@+id/chat_recycler_view" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="0dp" |
||||
|
android:layout_weight="1" |
||||
|
android:layout_marginBottom="16dp" |
||||
|
android:transcriptMode="alwaysScroll" /> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:orientation="horizontal"> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputLayout |
||||
|
android:layout_width="0dp" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_weight="1" |
||||
|
android:hint="@string/type_message"> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputEditText |
||||
|
android:id="@+id/chat_input" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:inputType="text" |
||||
|
android:imeOptions="actionSend" /> |
||||
|
|
||||
|
</com.google.android.material.textfield.TextInputLayout> |
||||
|
|
||||
|
<ImageButton |
||||
|
android:id="@+id/send_button" |
||||
|
android:layout_width="48dp" |
||||
|
android:layout_height="48dp" |
||||
|
android:layout_gravity="bottom" |
||||
|
android:background="?attr/selectableItemBackgroundBorderless" |
||||
|
android:src="@drawable/ic_send" |
||||
|
android:contentDescription="@string/send_message" /> |
||||
|
</LinearLayout> |
||||
|
</LinearLayout> |
||||
@ -0,0 +1,72 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
|
android:orientation="vertical" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content"> |
||||
|
|
||||
|
<com.google.android.material.bottomsheet.BottomSheetDragHandleView |
||||
|
android:id="@+id/drag_handle" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" /> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:orientation="vertical" |
||||
|
android:padding="16dp"> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/text_title" |
||||
|
android:text="@string/multiplayer" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:textAppearance="?attr/textAppearanceHeadline6" |
||||
|
android:gravity="center" |
||||
|
android:layout_marginTop="4dp" |
||||
|
android:textColor="?attr/colorOnSurface" /> |
||||
|
|
||||
|
<ImageView |
||||
|
android:layout_width="140dp" |
||||
|
android:layout_height="140dp" |
||||
|
android:layout_gravity="center" |
||||
|
android:layout_marginTop="16dp" |
||||
|
android:layout_marginBottom="24dp" |
||||
|
android:src="@drawable/ic_network" |
||||
|
app:tint="?attr/colorPrimary" /> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:orientation="horizontal" |
||||
|
android:layout_marginHorizontal="16dp" |
||||
|
android:layout_marginBottom="8dp"> |
||||
|
|
||||
|
<com.google.android.material.button.MaterialButton |
||||
|
android:id="@+id/btn_join" |
||||
|
style="@style/Widget.Material3.Button.TonalButton" |
||||
|
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" /> |
||||
|
|
||||
|
<Space |
||||
|
android:layout_width="16dp" |
||||
|
android:layout_height="match_parent" /> |
||||
|
|
||||
|
<com.google.android.material.button.MaterialButton |
||||
|
android:id="@+id/btn_create" |
||||
|
style="@style/Widget.Material3.Button" |
||||
|
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" /> |
||||
|
|
||||
|
</LinearLayout> |
||||
|
|
||||
|
</LinearLayout> |
||||
|
</LinearLayout> |
||||
@ -0,0 +1,75 @@ |
|||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="match_parent"> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:orientation="vertical" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content"> |
||||
|
|
||||
|
<com.google.android.material.bottomsheet.BottomSheetDragHandleView |
||||
|
android:id="@+id/drag_handle" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" /> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:orientation="vertical" |
||||
|
android:padding="16dp"> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/text_title" |
||||
|
android:text="@string/multiplayer" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:textAppearance="?attr/textAppearanceHeadline6" |
||||
|
android:gravity="center" |
||||
|
android:layout_marginTop="4dp" |
||||
|
android:textColor="?attr/colorOnSurface" /> |
||||
|
|
||||
|
<androidx.recyclerview.widget.RecyclerView |
||||
|
android:id="@+id/list_multiplayer" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_marginTop="16dp" |
||||
|
android:layout_marginBottom="8dp" /> |
||||
|
|
||||
|
<com.google.android.material.button.MaterialButton |
||||
|
android:id="@+id/btn_chat" |
||||
|
style="@style/Widget.Material3.Button.TonalButton" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_marginHorizontal="16dp" |
||||
|
android:layout_marginBottom="8dp" |
||||
|
android:enabled="true" |
||||
|
android:text="@string/multiplayer_chat" |
||||
|
app:icon="@drawable/ic_chat" |
||||
|
app:cornerRadius="16dp" /> |
||||
|
|
||||
|
<com.google.android.material.button.MaterialButton |
||||
|
android:id="@+id/btn_moderation" |
||||
|
style="@style/Widget.Material3.Button.TonalButton" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_marginHorizontal="16dp" |
||||
|
android:layout_marginBottom="8dp" |
||||
|
android:enabled="true" |
||||
|
android:text="@string/multiplayer_moderation" |
||||
|
app:cornerRadius="16dp" |
||||
|
app:icon="@drawable/ic_user" /> |
||||
|
|
||||
|
<com.google.android.material.button.MaterialButton |
||||
|
android:id="@+id/btn_leave" |
||||
|
style="@style/Widget.Material3.Button" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_marginHorizontal="16dp" |
||||
|
android:text="@string/multiplayer_exit_room" |
||||
|
app:icon="@drawable/ic_exit" |
||||
|
app:cornerRadius="16dp" /> |
||||
|
|
||||
|
</LinearLayout> |
||||
|
</LinearLayout> |
||||
|
</ScrollView> |
||||
@ -0,0 +1,119 @@ |
|||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="match_parent"> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:orientation="vertical" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:padding="16dp" |
||||
|
android:clipToPadding="false" |
||||
|
android:clipChildren="false" |
||||
|
android:elevation="4dp"> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/textTitle" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:textAppearance="?attr/textAppearanceHeadline6" |
||||
|
android:gravity="center" |
||||
|
android:paddingBottom="8dp" |
||||
|
android:textColor="?attr/colorOnSurface" /> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:hint="@string/multiplayer_ip_address" |
||||
|
android:padding="8dp"> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputEditText |
||||
|
android:id="@+id/ip_address" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:inputType="text" /> |
||||
|
</com.google.android.material.textfield.TextInputLayout> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:hint="@string/multiplayer_ip_port" |
||||
|
android:padding="8dp"> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputEditText |
||||
|
android:id="@+id/ip_port" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:inputType="number" /> |
||||
|
</com.google.android.material.textfield.TextInputLayout> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:hint="@string/multiplayer_username" |
||||
|
android:padding="8dp"> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputEditText |
||||
|
android:id="@+id/username" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:inputType="text" /> |
||||
|
</com.google.android.material.textfield.TextInputLayout> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:hint="@string/multiplayer_password" |
||||
|
android:padding="8dp"> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputEditText |
||||
|
android:id="@+id/password" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:inputType="textPassword" /> |
||||
|
</com.google.android.material.textfield.TextInputLayout> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:hint="@string/multiplayer_room_name" |
||||
|
android:padding="8dp"> |
||||
|
|
||||
|
<com.google.android.material.textfield.TextInputEditText |
||||
|
android:id="@+id/room_name" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:inputType="text" /> |
||||
|
</com.google.android.material.textfield.TextInputLayout> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:id="@+id/max_players_container" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:orientation="vertical"> |
||||
|
|
||||
|
<com.google.android.material.slider.Slider |
||||
|
android:id="@+id/max_players" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_margin="8dp" |
||||
|
android:value="8" |
||||
|
android:valueFrom="2" |
||||
|
android:valueTo="16" |
||||
|
android:stepSize="1" /> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/max_players_label" |
||||
|
android:layout_width="wrap_content" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_gravity="center" |
||||
|
android:text="@string/multiplayer_max_players_value" /> |
||||
|
</LinearLayout> |
||||
|
|
||||
|
<com.google.android.material.button.MaterialButton |
||||
|
android:id="@+id/btn_confirm" |
||||
|
android:layout_width="wrap_content" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:text="@android:string/ok" |
||||
|
android:layout_gravity="center" /> |
||||
|
</LinearLayout> |
||||
|
</ScrollView> |
||||
@ -0,0 +1,28 @@ |
|||||
|
<?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="horizontal" |
||||
|
android:padding="16dp" |
||||
|
android:gravity="center_vertical"> |
||||
|
|
||||
|
<ImageView |
||||
|
android:id="@+id/icon" |
||||
|
android:layout_width="24dp" |
||||
|
android:layout_height="24dp" |
||||
|
android:src="@drawable/ic_user" |
||||
|
android:layout_marginEnd="16dp"/> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/ban_text" |
||||
|
android:layout_width="0dp" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_weight="1"/> |
||||
|
|
||||
|
<com.google.android.material.button.MaterialButton |
||||
|
android:id="@+id/btn_unban" |
||||
|
style="@style/Widget.Material3.Button.TextButton" |
||||
|
android:layout_width="wrap_content" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:text="@string/multiplayer_unban"/> |
||||
|
</LinearLayout> |
||||
@ -0,0 +1,25 @@ |
|||||
|
<?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="horizontal" |
||||
|
android:gravity="center_vertical" |
||||
|
android:padding="8dp"> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/item_button_netplay_name" |
||||
|
android:layout_width="0dp" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_weight="1" |
||||
|
android:textAppearance="?attr/textAppearanceBodyLarge" /> |
||||
|
|
||||
|
<ImageButton |
||||
|
android:id="@+id/item_button_more" |
||||
|
android:layout_width="48dp" |
||||
|
android:layout_height="48dp" |
||||
|
android:background="?attr/selectableItemBackgroundBorderless" |
||||
|
android:contentDescription="@string/multiplayer_more_options" |
||||
|
android:src="@drawable/ic_more_vert" |
||||
|
android:padding="12dp" /> |
||||
|
|
||||
|
</LinearLayout> |
||||
@ -0,0 +1,36 @@ |
|||||
|
<?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="horizontal" |
||||
|
android:padding="8dp"> |
||||
|
|
||||
|
<ImageView |
||||
|
android:id="@+id/user_icon" |
||||
|
android:layout_width="24dp" |
||||
|
android:layout_height="24dp" |
||||
|
android:layout_marginEnd="8dp" /> |
||||
|
|
||||
|
<LinearLayout |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:orientation="vertical"> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/username_text" |
||||
|
android:layout_width="wrap_content" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:textStyle="bold" /> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/message_text" |
||||
|
android:layout_width="wrap_content" |
||||
|
android:layout_height="wrap_content" /> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/timestamp_text" |
||||
|
android:layout_width="wrap_content" |
||||
|
android:layout_height="wrap_content" /> |
||||
|
|
||||
|
</LinearLayout> |
||||
|
</LinearLayout> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<View xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="1dp" |
||||
|
android:background="?android:attr/listDivider"/> |
||||
@ -0,0 +1,18 @@ |
|||||
|
<?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="horizontal" |
||||
|
android:padding="16dp"> |
||||
|
|
||||
|
<ImageView |
||||
|
android:id="@+id/item_icon" |
||||
|
android:layout_width="24dp" |
||||
|
android:layout_height="24dp" |
||||
|
android:layout_marginEnd="16dp"/> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/item_text_netplay_name" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content"/> |
||||
|
</LinearLayout> |
||||
@ -0,0 +1,7 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<com.google.android.material.divider.MaterialDivider |
||||
|
xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_marginHorizontal="16dp" |
||||
|
android:layout_marginVertical="8dp" /> |
||||
@ -0,0 +1,24 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:orientation="horizontal" |
||||
|
android:padding="12dp" |
||||
|
android:gravity="center_vertical"> |
||||
|
|
||||
|
<ImageView |
||||
|
android:id="@+id/item_icon" |
||||
|
android:layout_width="24dp" |
||||
|
android:layout_height="24dp" |
||||
|
android:layout_marginEnd="16dp" |
||||
|
app:tint="?attr/colorPrimary" /> |
||||
|
|
||||
|
<TextView |
||||
|
android:id="@+id/item_text_netplay_name" |
||||
|
android:layout_width="0dp" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_weight="1" |
||||
|
android:textAppearance="?attr/textAppearanceBodyLarge" /> |
||||
|
|
||||
|
</LinearLayout> |
||||
@ -0,0 +1,13 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
<item |
||||
|
android:id="@+id/action_kick" |
||||
|
android:title="@string/multiplayer_kick_member" |
||||
|
android:enabled="false" /> |
||||
|
|
||||
|
<item |
||||
|
android:id="@+id/action_ban" |
||||
|
android:title="@string/multiplayer_ban" |
||||
|
android:enabled="false" /> |
||||
|
|
||||
|
</menu> |
||||
@ -0,0 +1,353 @@ |
|||||
|
// 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-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
|
#include "common/android/id_cache.h"
|
||||
|
#include "multiplayer.h"
|
||||
|
|
||||
|
#include "common/android/android_common.h"
|
||||
|
|
||||
|
#include "core/core.h"
|
||||
|
#include "network/network.h"
|
||||
|
#include "android/log.h"
|
||||
|
|
||||
|
|
||||
|
#include <thread>
|
||||
|
#include <chrono>
|
||||
|
|
||||
|
namespace IDCache = Common::Android; |
||||
|
Network::RoomNetwork* room_network; |
||||
|
|
||||
|
void AddNetPlayMessage(jint type, jstring msg) { |
||||
|
IDCache::GetEnvForThread()->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), |
||||
|
IDCache::GetAddNetPlayMessage(), type, msg); |
||||
|
} |
||||
|
|
||||
|
void AddNetPlayMessage(int type, const std::string& msg) { |
||||
|
JNIEnv* env = IDCache::GetEnvForThread(); |
||||
|
AddNetPlayMessage(type, Common::Android::ToJString(env, msg)); |
||||
|
} |
||||
|
|
||||
|
void ClearChat() { |
||||
|
IDCache::GetEnvForThread()->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), |
||||
|
IDCache::ClearChat()); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
bool NetworkInit(Network::RoomNetwork* room_network_) { |
||||
|
room_network = room_network_; |
||||
|
bool result = room_network->Init(); |
||||
|
|
||||
|
if (!result) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (auto member = room_network->GetRoomMember().lock()) { |
||||
|
// register the network structs to use in slots and signals
|
||||
|
member->BindOnStateChanged([](const Network::RoomMember::State& state) { |
||||
|
if (state == Network::RoomMember::State::Joined || |
||||
|
state == Network::RoomMember::State::Moderator) { |
||||
|
NetPlayStatus status; |
||||
|
std::string msg; |
||||
|
switch (state) { |
||||
|
case Network::RoomMember::State::Joined: |
||||
|
status = NetPlayStatus::ROOM_JOINED; |
||||
|
break; |
||||
|
case Network::RoomMember::State::Moderator: |
||||
|
status = NetPlayStatus::ROOM_MODERATOR; |
||||
|
break; |
||||
|
default: |
||||
|
return; |
||||
|
} |
||||
|
AddNetPlayMessage(static_cast<int>(status), msg); |
||||
|
} |
||||
|
}); |
||||
|
member->BindOnError([](const Network::RoomMember::Error& error) { |
||||
|
NetPlayStatus status; |
||||
|
std::string msg; |
||||
|
switch (error) { |
||||
|
case Network::RoomMember::Error::LostConnection: |
||||
|
status = NetPlayStatus::LOST_CONNECTION; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::HostKicked: |
||||
|
status = NetPlayStatus::HOST_KICKED; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::UnknownError: |
||||
|
status = NetPlayStatus::UNKNOWN_ERROR; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::NameCollision: |
||||
|
status = NetPlayStatus::NAME_COLLISION; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::IpCollision: |
||||
|
status = NetPlayStatus::MAC_COLLISION; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::WrongVersion: |
||||
|
status = NetPlayStatus::WRONG_VERSION; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::WrongPassword: |
||||
|
status = NetPlayStatus::WRONG_PASSWORD; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::CouldNotConnect: |
||||
|
status = NetPlayStatus::COULD_NOT_CONNECT; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::RoomIsFull: |
||||
|
status = NetPlayStatus::ROOM_IS_FULL; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::HostBanned: |
||||
|
status = NetPlayStatus::HOST_BANNED; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::PermissionDenied: |
||||
|
status = NetPlayStatus::PERMISSION_DENIED; |
||||
|
break; |
||||
|
case Network::RoomMember::Error::NoSuchUser: |
||||
|
status = NetPlayStatus::NO_SUCH_USER; |
||||
|
break; |
||||
|
} |
||||
|
AddNetPlayMessage(static_cast<int>(status), msg); |
||||
|
}); |
||||
|
member->BindOnStatusMessageReceived([](const Network::StatusMessageEntry& status_message) { |
||||
|
NetPlayStatus status = NetPlayStatus::NO_ERROR; |
||||
|
std::string msg(status_message.nickname); |
||||
|
switch (status_message.type) { |
||||
|
case Network::IdMemberJoin: |
||||
|
status = NetPlayStatus::MEMBER_JOIN; |
||||
|
break; |
||||
|
case Network::IdMemberLeave: |
||||
|
status = NetPlayStatus::MEMBER_LEAVE; |
||||
|
break; |
||||
|
case Network::IdMemberKicked: |
||||
|
status = NetPlayStatus::MEMBER_KICKED; |
||||
|
break; |
||||
|
case Network::IdMemberBanned: |
||||
|
status = NetPlayStatus::MEMBER_BANNED; |
||||
|
break; |
||||
|
case Network::IdAddressUnbanned: |
||||
|
status = NetPlayStatus::ADDRESS_UNBANNED; |
||||
|
break; |
||||
|
} |
||||
|
AddNetPlayMessage(static_cast<int>(status), msg); |
||||
|
}); |
||||
|
member->BindOnChatMessageReceived([](const Network::ChatEntry& chat) { |
||||
|
NetPlayStatus status = NetPlayStatus::CHAT_MESSAGE; |
||||
|
std::string msg(chat.nickname); |
||||
|
msg += ": "; |
||||
|
msg += chat.message; |
||||
|
AddNetPlayMessage(static_cast<int>(status), msg); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
NetPlayStatus NetPlayCreateRoom(const std::string& ipaddress, int port, |
||||
|
const std::string& username, const std::string& password, |
||||
|
const std::string& room_name, int max_players) { |
||||
|
|
||||
|
__android_log_print(ANDROID_LOG_INFO, "NetPlay", "NetPlayCreateRoom called with ipaddress: %s, port: %d, username: %s, room_name: %s, max_players: %d", ipaddress.c_str(), port, username.c_str(), room_name.c_str(), max_players); |
||||
|
|
||||
|
auto member = room_network->GetRoomMember().lock(); |
||||
|
if (!member) { |
||||
|
return NetPlayStatus::NETWORK_ERROR; |
||||
|
} |
||||
|
|
||||
|
if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) { |
||||
|
return NetPlayStatus::ALREADY_IN_ROOM; |
||||
|
} |
||||
|
|
||||
|
auto room = room_network->GetRoom().lock(); |
||||
|
if (!room) { |
||||
|
return NetPlayStatus::NETWORK_ERROR; |
||||
|
} |
||||
|
|
||||
|
if (room_name.length() < 3 || room_name.length() > 20) { |
||||
|
return NetPlayStatus::CREATE_ROOM_ERROR; |
||||
|
} |
||||
|
|
||||
|
// Placeholder game info
|
||||
|
const AnnounceMultiplayerRoom::GameInfo game{ |
||||
|
.name = "Default Game", |
||||
|
.id = 0, // Default program ID
|
||||
|
}; |
||||
|
|
||||
|
port = (port == 0) ? Network::DefaultRoomPort : static_cast<u16>(port); |
||||
|
|
||||
|
if (!room->Create(room_name, "", ipaddress, static_cast<u16>(port), password, |
||||
|
static_cast<u32>(std::min(max_players, 16)), username, game, nullptr, {})) { |
||||
|
return NetPlayStatus::CREATE_ROOM_ERROR; |
||||
|
} |
||||
|
|
||||
|
// Failsafe timer to avoid joining before creation
|
||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
||||
|
|
||||
|
member->Join(username, ipaddress.c_str(), static_cast<u16>(port), 0, Network::NoPreferredIP, password, ""); |
||||
|
|
||||
|
// Failsafe timer to avoid joining before creation
|
||||
|
for (int i = 0; i < 5; i++) { |
||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
||||
|
if (member->GetState() == Network::RoomMember::State::Joined || |
||||
|
member->GetState() == Network::RoomMember::State::Moderator) { |
||||
|
return NetPlayStatus::NO_ERROR; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// If join failed while room is created, clean up the room
|
||||
|
room->Destroy(); |
||||
|
return NetPlayStatus::CREATE_ROOM_ERROR; |
||||
|
} |
||||
|
|
||||
|
NetPlayStatus NetPlayJoinRoom(const std::string& ipaddress, int port, |
||||
|
const std::string& username, const std::string& password) { |
||||
|
auto member = room_network->GetRoomMember().lock(); |
||||
|
if (!member) { |
||||
|
return NetPlayStatus::NETWORK_ERROR; |
||||
|
} |
||||
|
|
||||
|
port = |
||||
|
(port == 0) ? Network::DefaultRoomPort : static_cast<u16>(port); |
||||
|
|
||||
|
|
||||
|
if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) { |
||||
|
return NetPlayStatus::ALREADY_IN_ROOM; |
||||
|
} |
||||
|
|
||||
|
member->Join(username, ipaddress.c_str(), static_cast<u16>(port), 0, Network::NoPreferredIP, password, ""); |
||||
|
|
||||
|
// Wait a bit for the connection and join process to complete
|
||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(500)); |
||||
|
|
||||
|
if (member->GetState() == Network::RoomMember::State::Joined || |
||||
|
member->GetState() == Network::RoomMember::State::Moderator) { |
||||
|
return NetPlayStatus::NO_ERROR; |
||||
|
} |
||||
|
|
||||
|
if (!member->IsConnected()) { |
||||
|
return NetPlayStatus::COULD_NOT_CONNECT; |
||||
|
} |
||||
|
|
||||
|
return NetPlayStatus::WRONG_PASSWORD; |
||||
|
} |
||||
|
|
||||
|
void NetPlaySendMessage(const std::string& msg) { |
||||
|
if (auto room = room_network->GetRoomMember().lock()) { |
||||
|
if (room->GetState() != Network::RoomMember::State::Joined && |
||||
|
room->GetState() != Network::RoomMember::State::Moderator) { |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
room->SendChatMessage(msg); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void NetPlayKickUser(const std::string& username) { |
||||
|
if (auto room = room_network->GetRoomMember().lock()) { |
||||
|
auto members = room->GetMemberInformation(); |
||||
|
auto it = std::find_if(members.begin(), members.end(), |
||||
|
[&username](const Network::RoomMember::MemberInformation& member) { |
||||
|
return member.nickname == username; |
||||
|
}); |
||||
|
if (it != members.end()) { |
||||
|
room->SendModerationRequest(Network::RoomMessageTypes::IdModKick, username); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void NetPlayBanUser(const std::string& username) { |
||||
|
if (auto room = room_network->GetRoomMember().lock()) { |
||||
|
auto members = room->GetMemberInformation(); |
||||
|
auto it = std::find_if(members.begin(), members.end(), |
||||
|
[&username](const Network::RoomMember::MemberInformation& member) { |
||||
|
return member.nickname == username; |
||||
|
}); |
||||
|
if (it != members.end()) { |
||||
|
room->SendModerationRequest(Network::RoomMessageTypes::IdModBan, username); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void NetPlayUnbanUser(const std::string& username) { |
||||
|
if (auto room = room_network->GetRoomMember().lock()) { |
||||
|
room->SendModerationRequest(Network::RoomMessageTypes::IdModUnban, username); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
std::vector<std::string> NetPlayRoomInfo() { |
||||
|
std::vector<std::string> info_list; |
||||
|
if (auto room = room_network->GetRoomMember().lock()) { |
||||
|
auto members = room->GetMemberInformation(); |
||||
|
if (!members.empty()) { |
||||
|
// name and max players
|
||||
|
auto room_info = room->GetRoomInformation(); |
||||
|
info_list.push_back(room_info.name + "|" + std::to_string(room_info.member_slots)); |
||||
|
// all members
|
||||
|
for (const auto& member : members) { |
||||
|
info_list.push_back(member.nickname); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return info_list; |
||||
|
} |
||||
|
|
||||
|
bool NetPlayIsJoined() { |
||||
|
auto member = room_network->GetRoomMember().lock(); |
||||
|
if (!member) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return (member->GetState() == Network::RoomMember::State::Joined || |
||||
|
member->GetState() == Network::RoomMember::State::Moderator); |
||||
|
} |
||||
|
|
||||
|
bool NetPlayIsHostedRoom() { |
||||
|
if (auto room = room_network->GetRoom().lock()) { |
||||
|
return room->GetState() == Network::Room::State::Open; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
void NetPlayLeaveRoom() { |
||||
|
if (auto room = room_network->GetRoom().lock()) { |
||||
|
// if you are in a room, leave it
|
||||
|
if (auto member = room_network->GetRoomMember().lock()) { |
||||
|
member->Leave(); |
||||
|
} |
||||
|
|
||||
|
ClearChat(); |
||||
|
|
||||
|
// if you are hosting a room, also stop hosting
|
||||
|
if (room->GetState() == Network::Room::State::Open) { |
||||
|
room->Destroy(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void NetworkShutdown() { |
||||
|
room_network->Shutdown(); |
||||
|
} |
||||
|
|
||||
|
bool NetPlayIsModerator() { |
||||
|
auto member = room_network->GetRoomMember().lock(); |
||||
|
if (!member) { |
||||
|
return false; |
||||
|
} |
||||
|
return member->GetState() == Network::RoomMember::State::Moderator; |
||||
|
} |
||||
|
|
||||
|
std::vector<std::string> NetPlayGetBanList() { |
||||
|
std::vector<std::string> ban_list; |
||||
|
if (auto room = room_network->GetRoom().lock()) { |
||||
|
auto [username_bans, ip_bans] = room->GetBanList(); |
||||
|
|
||||
|
// Add username bans
|
||||
|
for (const auto& username : username_bans) { |
||||
|
ban_list.push_back(username); |
||||
|
} |
||||
|
|
||||
|
// Add IP bans
|
||||
|
for (const auto& ip : ip_bans) { |
||||
|
ban_list.push_back(ip); |
||||
|
} |
||||
|
} |
||||
|
return ban_list; |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
// 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-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <string> |
||||
|
#include <vector> |
||||
|
|
||||
|
#include <common/common_types.h> |
||||
|
#include <network/network.h> |
||||
|
|
||||
|
enum class NetPlayStatus : s32 { |
||||
|
NO_ERROR, |
||||
|
|
||||
|
NETWORK_ERROR, |
||||
|
LOST_CONNECTION, |
||||
|
NAME_COLLISION, |
||||
|
MAC_COLLISION, |
||||
|
CONSOLE_ID_COLLISION, |
||||
|
WRONG_VERSION, |
||||
|
WRONG_PASSWORD, |
||||
|
COULD_NOT_CONNECT, |
||||
|
ROOM_IS_FULL, |
||||
|
HOST_BANNED, |
||||
|
PERMISSION_DENIED, |
||||
|
NO_SUCH_USER, |
||||
|
ALREADY_IN_ROOM, |
||||
|
CREATE_ROOM_ERROR, |
||||
|
HOST_KICKED, |
||||
|
UNKNOWN_ERROR, |
||||
|
|
||||
|
ROOM_UNINITIALIZED, |
||||
|
ROOM_IDLE, |
||||
|
ROOM_JOINING, |
||||
|
ROOM_JOINED, |
||||
|
ROOM_MODERATOR, |
||||
|
|
||||
|
MEMBER_JOIN, |
||||
|
MEMBER_LEAVE, |
||||
|
MEMBER_KICKED, |
||||
|
MEMBER_BANNED, |
||||
|
ADDRESS_UNBANNED, |
||||
|
|
||||
|
CHAT_MESSAGE, |
||||
|
}; |
||||
|
|
||||
|
bool NetworkInit(Network::RoomNetwork* room_network); |
||||
|
NetPlayStatus NetPlayCreateRoom(const std::string& ipaddress, int port, |
||||
|
const std::string& username, const std::string& password, |
||||
|
const std::string& room_name, int max_players); |
||||
|
NetPlayStatus NetPlayJoinRoom(const std::string& ipaddress, int port, |
||||
|
const std::string& username, const std::string& password); |
||||
|
std::vector<std::string> NetPlayRoomInfo(); |
||||
|
bool NetPlayIsJoined(); |
||||
|
bool NetPlayIsHostedRoom(); |
||||
|
bool NetPlayIsModerator(); |
||||
|
void NetPlaySendMessage(const std::string& msg); |
||||
|
void NetPlayKickUser(const std::string& username); |
||||
|
void NetPlayBanUser(const std::string& username); |
||||
|
void NetPlayLeaveRoom(); |
||||
|
std::string NetPlayGetConsoleId(); |
||||
|
void NetworkShutdown(); |
||||
|
std::vector<std::string> NetPlayGetBanList(); |
||||
|
void NetPlayUnbanUser(const std::string& username); |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue