mirror of
https://git.citron-emu.org/Citron/Citron.git
synced 2025-01-31 21:26:57 +01:00
android: Initial multiplayer room creation support (WIP)
Co-Authored-By: zhang wei <zwdreams@gmail.com> Co-Authored-By: Gamer64 <76565986+Gamer64ytb@users.noreply.github.com>
This commit is contained in:
parent
613099703a
commit
342c7a0c36
43 changed files with 2023 additions and 4 deletions
|
@ -11,6 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
<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-feature android:name="android.hardware.vulkan.version" android:version="0x401000" android:required="true" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.citron.citron_emu.utils.Log
|
||||||
import org.citron.citron_emu.model.InstallResult
|
import org.citron.citron_emu.model.InstallResult
|
||||||
import org.citron.citron_emu.model.Patch
|
import org.citron.citron_emu.model.Patch
|
||||||
import org.citron.citron_emu.model.GameVerificationResult
|
import org.citron.citron_emu.model.GameVerificationResult
|
||||||
|
import org.citron.citron_emu.utils.NetPlayManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class which contains methods that interact
|
* Class which contains methods that interact
|
||||||
|
@ -307,6 +308,24 @@ object NativeLibrary {
|
||||||
sEmulationActivity.clear()
|
sEmulationActivity.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun addNetPlayMessage(type: Int, message: String) {
|
||||||
|
val emulationActivity = sEmulationActivity.get()
|
||||||
|
if (emulationActivity != null) {
|
||||||
|
emulationActivity.addNetPlayMessages(type, message)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
NetPlayManager.addNetPlayMessage(type, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
@JvmStatic
|
||||||
|
fun clearChat() {
|
||||||
|
NetPlayManager.clearChat()
|
||||||
|
}
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun onEmulationStarted() {
|
fun onEmulationStarted() {
|
||||||
|
|
|
@ -39,6 +39,7 @@ import org.citron.citron_emu.NativeLibrary
|
||||||
import org.citron.citron_emu.R
|
import org.citron.citron_emu.R
|
||||||
import org.citron.citron_emu.CitronApplication
|
import org.citron.citron_emu.CitronApplication
|
||||||
import org.citron.citron_emu.databinding.ActivityEmulationBinding
|
import org.citron.citron_emu.databinding.ActivityEmulationBinding
|
||||||
|
import org.citron.citron_emu.dialogs.NetPlayDialog
|
||||||
import org.citron.citron_emu.features.input.NativeInput
|
import org.citron.citron_emu.features.input.NativeInput
|
||||||
import org.citron.citron_emu.features.settings.model.BooleanSetting
|
import org.citron.citron_emu.features.settings.model.BooleanSetting
|
||||||
import org.citron.citron_emu.features.settings.model.IntSetting
|
import org.citron.citron_emu.features.settings.model.IntSetting
|
||||||
|
@ -49,6 +50,7 @@ import org.citron.citron_emu.utils.InputHandler
|
||||||
import org.citron.citron_emu.utils.Log
|
import org.citron.citron_emu.utils.Log
|
||||||
import org.citron.citron_emu.utils.MemoryUtil
|
import org.citron.citron_emu.utils.MemoryUtil
|
||||||
import org.citron.citron_emu.utils.NativeConfig
|
import org.citron.citron_emu.utils.NativeConfig
|
||||||
|
import org.citron.citron_emu.utils.NetPlayManager
|
||||||
import org.citron.citron_emu.utils.NfcReader
|
import org.citron.citron_emu.utils.NfcReader
|
||||||
import org.citron.citron_emu.utils.ParamPackage
|
import org.citron.citron_emu.utils.ParamPackage
|
||||||
import org.citron.citron_emu.utils.ThemeHelper
|
import org.citron.citron_emu.utils.ThemeHelper
|
||||||
|
@ -317,6 +319,15 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||||
return this.apply { aspectRatio?.let { setAspectRatio(it) } }
|
return this.apply { aspectRatio?.let { setAspectRatio(it) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun displayMultiplayerDialog() {
|
||||||
|
val dialog = NetPlayDialog(this)
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addNetPlayMessages(type: Int, msg: String) {
|
||||||
|
NetPlayManager.addNetPlayMessage(type, msg)
|
||||||
|
}
|
||||||
|
|
||||||
private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder():
|
private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder():
|
||||||
PictureInPictureParams.Builder {
|
PictureInPictureParams.Builder {
|
||||||
val pictureInPictureActions: MutableList<RemoteAction> = mutableListOf()
|
val pictureInPictureActions: MutableList<RemoteAction> = mutableListOf()
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
package org.citron.citron_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.citron.citron_emu.R
|
||||||
|
import org.citron.citron_emu.databinding.DialogChatBinding
|
||||||
|
import org.citron.citron_emu.databinding.ItemChatMessageBinding
|
||||||
|
import org.citron.citron_emu.utils.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.timestampText.text = message.timestamp
|
||||||
|
binding.messageText.text = message.message
|
||||||
|
binding.userIcon.setImageResource(when (message.nickname) {
|
||||||
|
"System" -> R.drawable.ic_system
|
||||||
|
else -> R.drawable.ic_user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,397 @@
|
||||||
|
// Copyright 2024 Mandarine Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citron.citron_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.util.Log
|
||||||
|
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.citron.citron_emu.CitronApplication
|
||||||
|
import org.citron.citron_emu.R
|
||||||
|
import org.citron.citron_emu.databinding.DialogMultiplayerConnectBinding
|
||||||
|
import org.citron.citron_emu.databinding.DialogMultiplayerLobbyBinding
|
||||||
|
import org.citron.citron_emu.databinding.DialogMultiplayerRoomBinding
|
||||||
|
import org.citron.citron_emu.databinding.ItemBanListBinding
|
||||||
|
import org.citron.citron_emu.databinding.ItemButtonNetplayBinding
|
||||||
|
import org.citron.citron_emu.databinding.ItemTextNetplayBinding
|
||||||
|
import org.citron.citron_emu.utils.CompatUtils
|
||||||
|
import org.citron.citron_emu.utils.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_joined
|
||||||
|
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 {
|
||||||
|
inflate(R.menu.menu_netplay_member)
|
||||||
|
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(
|
||||||
|
CitronApplication.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(android.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(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = BanListAdapter(banList, onUnban)
|
||||||
|
recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
dialog.setView(view)
|
||||||
|
dialog.setPositiveButton(android.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_ip)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -279,6 +279,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.menu_multiplayer -> {
|
||||||
|
emulationActivity?.displayMultiplayerDialog()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.menu_lock_drawer -> {
|
R.id.menu_lock_drawer -> {
|
||||||
when (IntSetting.LOCK_DRAWER.getInt()) {
|
when (IntSetting.LOCK_DRAWER.getInt()) {
|
||||||
DrawerLayout.LOCK_MODE_UNLOCKED -> {
|
DrawerLayout.LOCK_MODE_UNLOCKED -> {
|
||||||
|
|
|
@ -155,6 +155,14 @@ class HomeSettingsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
add(
|
||||||
|
HomeSetting(
|
||||||
|
R.string.multiplayer,
|
||||||
|
R.string.multiplayer_description,
|
||||||
|
R.drawable.ic_multiplayer,
|
||||||
|
{ mainActivity.displayMultiplayerDialog() }
|
||||||
|
),
|
||||||
|
)
|
||||||
add(
|
add(
|
||||||
HomeSetting(
|
HomeSetting(
|
||||||
R.string.verify_installed_content,
|
R.string.verify_installed_content,
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.citron.citron_emu.HomeNavigationDirections
|
||||||
import org.citron.citron_emu.NativeLibrary
|
import org.citron.citron_emu.NativeLibrary
|
||||||
import org.citron.citron_emu.R
|
import org.citron.citron_emu.R
|
||||||
import org.citron.citron_emu.databinding.ActivityMainBinding
|
import org.citron.citron_emu.databinding.ActivityMainBinding
|
||||||
|
import org.citron.citron_emu.dialogs.NetPlayDialog
|
||||||
import org.citron.citron_emu.features.settings.model.Settings
|
import org.citron.citron_emu.features.settings.model.Settings
|
||||||
import org.citron.citron_emu.fragments.AddGameFolderDialogFragment
|
import org.citron.citron_emu.fragments.AddGameFolderDialogFragment
|
||||||
import org.citron.citron_emu.fragments.ProgressDialogFragment
|
import org.citron.citron_emu.fragments.ProgressDialogFragment
|
||||||
|
@ -178,6 +179,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
showNavigation(visible = true, animated = true)
|
showNavigation(visible = true, animated = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun displayMultiplayerDialog() {
|
||||||
|
val dialog = NetPlayDialog(this)
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun setUpNavigation(navController: NavController) {
|
private fun setUpNavigation(navController: NavController) {
|
||||||
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
|
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright 2024 Mandarine Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citron.citron_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,223 @@
|
||||||
|
// Copyright 2024 Mandarine Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
package org.citron.citron_emu.utils
|
||||||
|
|
||||||
|
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.citron.citron_emu.CitronApplication
|
||||||
|
import org.citron.citron_emu.R
|
||||||
|
import org.citron.citron_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 = "Citron${(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 = CitronApplication.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> {
|
||||||
|
Log.info("Netplay Ban ${netPlayGetBanList()}.toList()")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ add_library(citron-android SHARED
|
||||||
android_config.cpp
|
android_config.cpp
|
||||||
android_config.h
|
android_config.h
|
||||||
native_input.cpp
|
native_input.cpp
|
||||||
|
multiplayer.cpp
|
||||||
|
multiplayer.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set_property(TARGET citron-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
|
set_property(TARGET citron-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
|
||||||
|
|
334
src/android/app/src/main/jni/multiplayer.cpp
Normal file
334
src/android/app/src/main/jni/multiplayer.cpp
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
// Copyright 2025 Citra Project / Mandarine Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
#include "multiplayer.h"
|
||||||
|
#include "core/core.h"
|
||||||
|
#include "network/network.h"
|
||||||
|
#include "common/android/android_common.h"
|
||||||
|
#include "common/android/id_cache.h"
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
void AddNetPlayMessage(jint type, jstring msg) {
|
||||||
|
Common::Android::GetEnvForThread()->CallStaticVoidMethod(Common::Android::GetNativeLibraryClass(),
|
||||||
|
reinterpret_cast<jmethodID>(AddNetPlayMessage), type, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddNetPlayMessage(int type, const std::string& msg) {
|
||||||
|
JNIEnv* env = Common::Android::GetEnvForThread();
|
||||||
|
jmethodID method_id = env->GetStaticMethodID(Common::Android::GetNativeLibraryClass(), "addNetPlayMessage", "(ILjava/lang/String;)V");
|
||||||
|
env->CallStaticVoidMethod(Common::Android::GetNativeLibraryClass(), method_id, type, Common::Android::ToJString(env, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearChat() {
|
||||||
|
JNIEnv* env = Common::Android::GetEnvForThread();
|
||||||
|
jmethodID method_id = env->GetStaticMethodID(Common::Android::GetNativeLibraryClass(), "clearChat", "()V");
|
||||||
|
env->CallStaticVoidMethod(Common::Android::GetNativeLibraryClass(), method_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetworkInit() {
|
||||||
|
bool result = Network::RoomNetwork().Init();
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto member = Network::RoomNetwork().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::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;
|
||||||
|
case Network::RoomMember::Error::IpCollision:
|
||||||
|
status = NetPlayStatus::MAC_COLLISION;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnnounceMultiplayerRoom::GameInfo game{
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
NetPlayStatus NetPlayCreateRoom(const std::string& ipaddress, int port, const std::string& username,
|
||||||
|
const std::string& password, const std::string& room_name,
|
||||||
|
int max_players) {
|
||||||
|
|
||||||
|
auto member = Network::RoomNetwork().GetRoomMember().lock();
|
||||||
|
if (!member) {
|
||||||
|
return NetPlayStatus::NETWORK_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) {
|
||||||
|
return NetPlayStatus::ALREADY_IN_ROOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto room = Network::RoomNetwork().GetRoom().lock();
|
||||||
|
if (!room) {
|
||||||
|
return NetPlayStatus::NETWORK_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room_name.length() < 3 || room_name.length() > 20) {
|
||||||
|
return NetPlayStatus::CREATE_ROOM_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!room->Create(room_name, "", ipaddress, port, password, std::min(max_players, 16), username,
|
||||||
|
game, std::make_unique<Network::VerifyUser::NullBackend>(), {})) {
|
||||||
|
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(), 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 = Network::RoomNetwork().GetRoomMember().lock();
|
||||||
|
if (!member) {
|
||||||
|
return NetPlayStatus::NETWORK_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member->GetState() == Network::RoomMember::State::Joining || member->IsConnected()) {
|
||||||
|
return NetPlayStatus::ALREADY_IN_ROOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
member->Join(username, ipaddress.c_str(), 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 = Network::RoomNetwork().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 = Network::RoomNetwork().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 = Network::RoomNetwork().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 = Network::RoomNetwork().GetRoomMember().lock()) {
|
||||||
|
room->SendModerationRequest(Network::RoomMessageTypes::IdModUnban, username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> NetPlayRoomInfo() {
|
||||||
|
std::vector<std::string> info_list;
|
||||||
|
if (auto room = Network::RoomNetwork().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 = Network::RoomNetwork().GetRoomMember().lock();
|
||||||
|
if (!member) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (member->GetState() == Network::RoomMember::State::Joined ||
|
||||||
|
member->GetState() == Network::RoomMember::State::Moderator);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetPlayIsHostedRoom() {
|
||||||
|
if (auto room = Network::RoomNetwork().GetRoom().lock()) {
|
||||||
|
return room->GetState() == Network::Room::State::Open;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetPlayLeaveRoom() {
|
||||||
|
if (auto room = Network::RoomNetwork().GetRoom().lock()) {
|
||||||
|
// if you are in a room, leave it
|
||||||
|
if (auto member = Network::RoomNetwork().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() {
|
||||||
|
Network::RoomNetwork().Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NetPlayIsModerator() {
|
||||||
|
auto member = Network::RoomNetwork().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 = Network::RoomNetwork().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;
|
||||||
|
}
|
64
src/android/app/src/main/jni/multiplayer.h
Normal file
64
src/android/app/src/main/jni/multiplayer.h
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// Copyright 2025 Citra Project / Mandarine Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <common/common_types.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();
|
||||||
|
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);
|
|
@ -62,6 +62,7 @@
|
||||||
#include "video_core/renderer_vulkan/renderer_vulkan.h"
|
#include "video_core/renderer_vulkan/renderer_vulkan.h"
|
||||||
#include "video_core/vulkan_common/vulkan_instance.h"
|
#include "video_core/vulkan_common/vulkan_instance.h"
|
||||||
#include "video_core/vulkan_common/vulkan_surface.h"
|
#include "video_core/vulkan_common/vulkan_surface.h"
|
||||||
|
#include "multiplayer.h"
|
||||||
|
|
||||||
#define jconst [[maybe_unused]] const auto
|
#define jconst [[maybe_unused]] const auto
|
||||||
#define jauto [[maybe_unused]] auto
|
#define jauto [[maybe_unused]] auto
|
||||||
|
@ -705,6 +706,71 @@ void Java_org_citron_citron_1emu_NativeLibrary_setCabinetMode(JNIEnv* env, jclas
|
||||||
static_cast<Service::NFP::CabinetMode>(jcabinetMode));
|
static_cast<Service::NFP::CabinetMode>(jcabinetMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayCreateRoom(
|
||||||
|
JNIEnv* env, [[maybe_unused]] jobject obj, jstring ipaddress, jint port, jstring username,
|
||||||
|
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),
|
||||||
|
Common::Android::GetJString(env, room_name), max_players));
|
||||||
|
}
|
||||||
|
JNIEXPORT jint JNICALL Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayJoinRoom(
|
||||||
|
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,
|
||||||
|
Common::Android::GetJString(env, username), Common::Android::GetJString(env, password)));
|
||||||
|
}
|
||||||
|
JNIEXPORT jobjectArray JNICALL
|
||||||
|
Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayRoomInfo(
|
||||||
|
JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||||
|
return Common::Android::ToJStringArray(env, NetPlayRoomInfo());
|
||||||
|
}
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayIsJoined(
|
||||||
|
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||||
|
return NetPlayIsJoined();
|
||||||
|
}
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayIsHostedRoom(
|
||||||
|
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||||
|
return NetPlayIsHostedRoom();
|
||||||
|
}
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_org_citron_citron_1emu_utils_NetPlayManager_netPlaySendMessage(
|
||||||
|
JNIEnv* env, [[maybe_unused]] jobject obj, jstring msg) {
|
||||||
|
NetPlaySendMessage(Common::Android::GetJString(env, msg));
|
||||||
|
}
|
||||||
|
JNIEXPORT void JNICALL Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayKickUser(
|
||||||
|
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
|
||||||
|
NetPlayKickUser(Common::Android::GetJString(env, username));
|
||||||
|
}
|
||||||
|
JNIEXPORT void JNICALL Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayLeaveRoom(
|
||||||
|
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||||
|
NetPlayLeaveRoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayIsModerator(
|
||||||
|
[[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||||
|
return NetPlayIsModerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jobjectArray JNICALL
|
||||||
|
Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayGetBanList(
|
||||||
|
JNIEnv* env, [[maybe_unused]] jobject obj) {
|
||||||
|
return Common::Android::ToJStringArray(env, NetPlayGetBanList());
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayBanUser(
|
||||||
|
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
|
||||||
|
NetPlayBanUser(Common::Android::GetJString(env, username));
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_citron_citron_1emu_utils_NetPlayManager_netPlayUnbanUser(
|
||||||
|
JNIEnv* env, [[maybe_unused]] jobject obj, jstring username) {
|
||||||
|
NetPlayUnbanUser(Common::Android::GetJString(env, username));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
jboolean Java_org_citron_citron_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, jclass clazz) {
|
jboolean Java_org_citron_citron_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, jclass clazz) {
|
||||||
auto bis_system =
|
auto bis_system =
|
||||||
EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents();
|
EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents();
|
||||||
|
|
10
src/android/app/src/main/res/drawable/ic_chat.xml
Normal file
10
src/android/app/src/main/res/drawable/ic_chat.xml
Normal 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="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>
|
9
src/android/app/src/main/res/drawable/ic_ip.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_ip.xml
Normal 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>
|
10
src/android/app/src/main/res/drawable/ic_joined.xml
Normal file
10
src/android/app/src/main/res/drawable/ic_joined.xml
Normal 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>
|
|
@ -1,9 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportHeight="24"
|
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:width="24dp">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
android:fillColor="?attr/colorControlNormal"
|
android:fillColor="?attr/colorControlNormal"
|
||||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
|
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||||
</vector>
|
</vector>
|
9
src/android/app/src/main/res/drawable/ic_multiplayer.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_multiplayer.xml
Normal 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>
|
10
src/android/app/src/main/res/drawable/ic_network.xml
Normal file
10
src/android/app/src/main/res/drawable/ic_network.xml
Normal 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="?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>
|
9
src/android/app/src/main/res/drawable/ic_send.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_send.xml
Normal 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="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
|
||||||
|
</vector>
|
10
src/android/app/src/main/res/drawable/ic_system.xml
Normal file
10
src/android/app/src/main/res/drawable/ic_system.xml
Normal 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="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>
|
9
src/android/app/src/main/res/drawable/ic_user.xml
Normal file
9
src/android/app/src/main/res/drawable/ic_user.xml
Normal 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,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>
|
7
src/android/app/src/main/res/layout/dialog_ban_list.xml
Normal file
7
src/android/app/src/main/res/layout/dialog_ban_list.xml
Normal file
|
@ -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"/>
|
38
src/android/app/src/main/res/layout/dialog_bottom_sheet.xml
Normal file
38
src/android/app/src/main/res/layout/dialog_bottom_sheet.xml
Normal file
|
@ -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>
|
55
src/android/app/src/main/res/layout/dialog_chat.xml
Normal file
55
src/android/app/src/main/res/layout/dialog_chat.xml
Normal file
|
@ -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>
|
119
src/android/app/src/main/res/layout/dialog_multiplayer_room.xml
Normal file
119
src/android/app/src/main/res/layout/dialog_multiplayer_room.xml
Normal file
|
@ -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>
|
28
src/android/app/src/main/res/layout/item_ban_list.xml
Normal file
28
src/android/app/src/main/res/layout/item_ban_list.xml
Normal file
|
@ -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>
|
25
src/android/app/src/main/res/layout/item_button_netplay.xml
Normal file
25
src/android/app/src/main/res/layout/item_button_netplay.xml
Normal file
|
@ -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>
|
38
src/android/app/src/main/res/layout/item_chat_message.xml
Normal file
38
src/android/app/src/main/res/layout/item_chat_message.xml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?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/timestamp_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#888888" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/message_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
25
src/android/app/src/main/res/layout/item_netplay_button.xml
Normal file
25
src/android/app/src/main/res/layout/item_netplay_button.xml
Normal file
|
@ -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:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/item_button_netplay_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/item_button_netplay"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/multiplayer_kick_member"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/item_button_more"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:src="@drawable/ic_more_vert"/>
|
||||||
|
</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"/>
|
18
src/android/app/src/main/res/layout/item_netplay_text.xml
Normal file
18
src/android/app/src/main/res/layout/item_netplay_text.xml
Normal file
|
@ -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" />
|
24
src/android/app/src/main/res/layout/item_text_netplay.xml
Normal file
24
src/android/app/src/main/res/layout/item_text_netplay.xml
Normal file
|
@ -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>
|
|
@ -26,6 +26,11 @@
|
||||||
android:icon="@drawable/ic_overlay"
|
android:icon="@drawable/ic_overlay"
|
||||||
android:title="@string/emulation_input_overlay" />
|
android:title="@string/emulation_input_overlay" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_multiplayer"
|
||||||
|
android:icon="@drawable/ic_network"
|
||||||
|
android:title="@string/multiplayer" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_lock_drawer"
|
android:id="@+id/menu_lock_drawer"
|
||||||
android:icon="@drawable/ic_unlock"
|
android:icon="@drawable/ic_unlock"
|
||||||
|
|
13
src/android/app/src/main/res/menu/menu_netplay_member.xml
Normal file
13
src/android/app/src/main/res/menu/menu_netplay_member.xml
Normal file
|
@ -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>
|
|
@ -649,6 +649,74 @@
|
||||||
<string name="multiplier_eight">8x</string>
|
<string name="multiplier_eight">8x</string>
|
||||||
<string name="multiplier_sixteen">16x</string>
|
<string name="multiplier_sixteen">16x</string>
|
||||||
|
|
||||||
|
<!-- Multiplayer -->
|
||||||
|
<string name="multiplayer">Multiplayer</string>
|
||||||
|
<string name="multiplayer_description">Host your own game room or join an existing one to play with people</string>
|
||||||
|
<string name="multiplayer_room_title">Room: %1$s</string>
|
||||||
|
<string name="multiplayer_console_id">Console ID:%1$s</string>
|
||||||
|
<string name="multiplayer_create_room">Create</string>
|
||||||
|
<string name="multiplayer_join_room">Join</string>
|
||||||
|
<string name="multiplayer_username">Username</string>
|
||||||
|
<string name="multiplayer_ip_address">IP Address</string>
|
||||||
|
<string name="multiplayer_ip_port">Port</string>
|
||||||
|
<string name="multiplayer_create_room_success">Room created successfully!</string>
|
||||||
|
<string name="multiplayer_join_room_success">Join the room successfully!</string>
|
||||||
|
<string name="multiplayer_create_room_failed">Failed to create room!</string>
|
||||||
|
<string name="multiplayer_join_room_failed">Failed to join room!</string>
|
||||||
|
<string name="multiplayer_input_invalid">Invalid address or name is too short!</string>
|
||||||
|
<string name="multiplayer_port_invalid">Invalid port!</string>
|
||||||
|
<string name="multiplayer_exit_room">Exit Room</string>
|
||||||
|
<string name="multiplayer_network_error">Network error</string>
|
||||||
|
<string name="multiplayer_lost_connection">Lost connection</string>
|
||||||
|
<string name="multiplayer_name_collision">Name collision</string>
|
||||||
|
<string name="multiplayer_mac_collision">Mac collision</string>
|
||||||
|
<string name="multiplayer_console_id_collision">Console ID collision</string>
|
||||||
|
<string name="multiplayer_wrong_version">Wrong version</string>
|
||||||
|
<string name="multiplayer_wrong_password">Wrong password</string>
|
||||||
|
<string name="multiplayer_could_not_connect">Could not connect</string>
|
||||||
|
<string name="multiplayer_room_is_full">Room is full</string>
|
||||||
|
<string name="multiplayer_host_banned">Host banned</string>
|
||||||
|
<string name="multiplayer_permission_denied">Permission denied</string>
|
||||||
|
<string name="multiplayer_no_such_user">No such user</string>
|
||||||
|
<string name="multiplayer_already_in_room">Already in room</string>
|
||||||
|
<string name="multiplayer_create_room_error">Create room error</string>
|
||||||
|
<string name="multiplayer_host_kicked">Host kicked</string>
|
||||||
|
<string name="multiplayer_unknown_error">unknown error</string>
|
||||||
|
<string name="multiplayer_room_uninitialized">Room uninitialized</string>
|
||||||
|
<string name="multiplayer_room_idle">Room idle</string>
|
||||||
|
<string name="multiplayer_room_joining">Room joining</string>
|
||||||
|
<string name="multiplayer_room_joined">Room joined</string>
|
||||||
|
<string name="multiplayer_room_moderator">Room moderator</string>
|
||||||
|
<string name="multiplayer_member_join">%1$s joined</string>
|
||||||
|
<string name="multiplayer_member_leave">%1$s left</string>
|
||||||
|
<string name="multiplayer_member_kicked">%1$s kicked</string>
|
||||||
|
<string name="multiplayer_member_banned">%1$s banned</string>
|
||||||
|
<string name="multiplayer_address_unbanned">address unbanned</string>
|
||||||
|
<string name="multiplayer_kick_member">Kick Out</string>
|
||||||
|
<string name="multiplayer_chat_input_hint">Send messages……</string>
|
||||||
|
<string name="multiplayer_password">Password</string>
|
||||||
|
<string name="original_button_text">Join</string>
|
||||||
|
<string name="disabled_button_text">Joining...</string>
|
||||||
|
<string name="multiplayer_room_name">Room Name</string>
|
||||||
|
<string name="multiplayer_room_name_invalid">Room name must be between 3 and 20 characters</string>
|
||||||
|
<string name="multiplayer_max_players">Max Players (16)</string>
|
||||||
|
<string name="multiplayer_max_players_value">Max Players: %d</string>
|
||||||
|
<string name="multiplayer_chat">Chat</string>
|
||||||
|
<string name="multiplayer_more_options">More Options</string>
|
||||||
|
<string name="multiplayer_ip_copied">IP Address copied to clipboard</string>
|
||||||
|
<string name="multiplayer_server_address">Server Address</string>
|
||||||
|
<string name="chat">Chat</string>
|
||||||
|
<string name="type_message">Type message……</string>
|
||||||
|
<string name="send">Send</string>
|
||||||
|
<string name="send_message">Send Message</string>
|
||||||
|
<string name="multiplayer_moderation">Moderation</string>
|
||||||
|
<string name="multiplayer_moderation_title">Ban List</string>
|
||||||
|
<string name="multiplayer_no_bans">No banned users</string>
|
||||||
|
<string name="multiplayer_unban_title">Unban User</string>
|
||||||
|
<string name="multiplayer_unban">Unban</string>
|
||||||
|
<string name="multiplayer_unban_message">Are you sure you want to unban %1$s?</string>
|
||||||
|
<string name="multiplayer_ban">Ban User</string>
|
||||||
|
|
||||||
<!-- Black backgrounds theme -->
|
<!-- Black backgrounds theme -->
|
||||||
<string name="use_black_backgrounds">Black backgrounds</string>
|
<string name="use_black_backgrounds">Black backgrounds</string>
|
||||||
<string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
|
<string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
|
||||||
|
|
|
@ -38,6 +38,15 @@ jstring ToJString(JNIEnv* env, std::u16string_view str) {
|
||||||
return ToJString(env, Common::UTF16ToUTF8(str));
|
return ToJString(env, Common::UTF16ToUTF8(str));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobjectArray ToJStringArray(JNIEnv* env, const std::vector<std::string>& strs) {
|
||||||
|
jobjectArray array =
|
||||||
|
env->NewObjectArray(static_cast<jsize>(strs.size()), env->FindClass("java/lang/String"), env->NewStringUTF(""));
|
||||||
|
for (size_t i = 0; i < strs.size(); ++i) {
|
||||||
|
env->SetObjectArrayElement(array, static_cast<jsize>(i), ToJString(env, strs[i]));
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
double GetJDouble(JNIEnv* env, jobject jdouble) {
|
double GetJDouble(JNIEnv* env, jobject jdouble) {
|
||||||
return env->GetDoubleField(jdouble, GetDoubleValueField());
|
return env->GetDoubleField(jdouble, GetDoubleValueField());
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
namespace Common::Android {
|
namespace Common::Android {
|
||||||
|
|
||||||
std::string GetJString(JNIEnv* env, jstring jstr);
|
std::string GetJString(JNIEnv* env, jstring jstr);
|
||||||
|
jobjectArray ToJStringArray(JNIEnv* env, const std::vector<std::string>& strs);
|
||||||
jstring ToJString(JNIEnv* env, std::string_view str);
|
jstring ToJString(JNIEnv* env, std::string_view str);
|
||||||
jstring ToJString(JNIEnv* env, std::u16string_view str);
|
jstring ToJString(JNIEnv* env, std::u16string_view str);
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/fs/fs_android.h"
|
#include "common/fs/fs_android.h"
|
||||||
#include "video_core/rasterizer_interface.h"
|
#include "video_core/rasterizer_interface.h"
|
||||||
|
#include "android\app\src\main\jni\multiplayer.h"
|
||||||
|
|
||||||
static JavaVM* s_java_vm;
|
static JavaVM* s_java_vm;
|
||||||
static jclass s_native_library_class;
|
static jclass s_native_library_class;
|
||||||
|
@ -88,6 +89,8 @@ static jmethodID s_citron_input_device_get_supports_vibration;
|
||||||
static jmethodID s_citron_input_device_vibrate;
|
static jmethodID s_citron_input_device_vibrate;
|
||||||
static jmethodID s_citron_input_device_get_axes;
|
static jmethodID s_citron_input_device_get_axes;
|
||||||
static jmethodID s_citron_input_device_has_keys;
|
static jmethodID s_citron_input_device_has_keys;
|
||||||
|
static jmethodID s_add_netplay_message;
|
||||||
|
static jmethodID s_clear_chat;
|
||||||
|
|
||||||
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
||||||
|
|
||||||
|
@ -144,6 +147,15 @@ jmethodID GetOnEmulationStarted() {
|
||||||
return s_on_emulation_started;
|
return s_on_emulation_started;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jmethodID GetAddNetPlayMessage() {
|
||||||
|
return s_add_netplay_message;
|
||||||
|
}
|
||||||
|
|
||||||
|
jmethodID ClearChat() {
|
||||||
|
return s_clear_chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
jmethodID GetOnEmulationStopped() {
|
jmethodID GetOnEmulationStopped() {
|
||||||
return s_on_emulation_stopped;
|
return s_on_emulation_stopped;
|
||||||
}
|
}
|
||||||
|
@ -547,6 +559,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
s_citron_input_device_has_keys =
|
s_citron_input_device_has_keys =
|
||||||
env->GetMethodID(citron_input_device_interface, "hasKeys", "([I)[Z");
|
env->GetMethodID(citron_input_device_interface, "hasKeys", "([I)[Z");
|
||||||
env->DeleteLocalRef(citron_input_device_interface);
|
env->DeleteLocalRef(citron_input_device_interface);
|
||||||
|
s_add_netplay_message = env->GetStaticMethodID(s_native_library_class, "addNetPlayMessage",
|
||||||
|
"(ILjava/lang/String;)V");
|
||||||
|
s_clear_chat = env->GetStaticMethodID(s_native_library_class, "clearChat", "()V");
|
||||||
|
|
||||||
// Initialize Android Storage
|
// Initialize Android Storage
|
||||||
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
|
||||||
|
@ -554,6 +569,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||||
// Initialize applets
|
// Initialize applets
|
||||||
Common::Android::SoftwareKeyboard::InitJNI(env);
|
Common::Android::SoftwareKeyboard::InitJNI(env);
|
||||||
|
|
||||||
|
// Init network for multiplayer
|
||||||
|
NetworkInit();
|
||||||
|
|
||||||
return JNI_VERSION;
|
return JNI_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -582,6 +600,9 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
|
||||||
|
|
||||||
// UnInitialize applets
|
// UnInitialize applets
|
||||||
SoftwareKeyboard::CleanupJNI(env);
|
SoftwareKeyboard::CleanupJNI(env);
|
||||||
|
|
||||||
|
// Shutdown network for multiplayer
|
||||||
|
NetworkShutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|
Loading…
Reference in a new issue