Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/assets/jsons/translations/template.properties
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,8 @@ Welcome to Chat! =
Successfully connected to WebSocket server! =
Authentication issue detected! You have to set a password to use Chat. =
Type something... =
Everyone =
To: =

Error: [error] =
WebSocket connection closed. Cause: [cause] =
Expand Down
43 changes: 31 additions & 12 deletions core/src/com/unciv/logic/multiplayer/chat/ChatStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@ import java.util.LinkedList
import java.util.Queue
import java.util.UUID

data class Chat(
data class ChatMessageEntry(
val civName: String,
val message: String,
/** Null for public chat; set for private messages. */
val toCivName: String? = null,
Comment thread
Fanfblrik marked this conversation as resolved.
Outdated
)

class Chat(
val gameId: UUID,
) {
var unreadCount = 0

// <civName, message> pairs
private val messages: MutableList<Pair<String, String>> = mutableListOf(INITIAL_MESSAGE)
private val messages: MutableList<ChatMessageEntry> = mutableListOf(
ChatMessageEntry(INITIAL_MESSAGE.first, INITIAL_MESSAGE.second)
)

val length: Int get() = messages.size

Expand All @@ -26,22 +34,29 @@ data class Chat(
*
* The server will relay it back if a delivery was acknowledged and that is when we should display it.
*/
fun requestMessageSend(civName: String, message: String) {
fun requestMessageSend(
civName: String,
message: String,
toPlayerId: String? = null,
toCivName: String? = null,
) {
Gdx.app.postRunnable {
ChatWebSocket.requestMessageSend(Message.Chat(civName, message, gameId.toString()))
ChatWebSocket.requestMessageSend(
Message.Chat(civName, message, gameId.toString(), toPlayerId, toCivName)
)
}
}

/**
* Although public, this should only be called when a ChatMessageReceivedEvent is received once.
*/
fun addMessage(civName: String, message: String) {
messages.add(Pair(civName, message))
fun addMessage(civName: String, message: String, toCivName: String? = null) {
messages.add(ChatMessageEntry(civName, message, toCivName))
}

fun forEachMessage(action: (String, String) -> Unit) {
for ((civName, message) in messages) {
action(civName, message)
fun forEachMessage(action: (ChatMessageEntry) -> Unit) {
for (entry in messages) {
action(entry)
}
}

Expand Down Expand Up @@ -91,9 +106,13 @@ object ChatStore {
}

val chat = chatPopup?.chat ?: getChatByGameId(gameId)
chat.addMessage(incomingChatMsg.civName, incomingChatMsg.message)
chat.addMessage(incomingChatMsg.civName, incomingChatMsg.message, incomingChatMsg.toCivName)
if (gameId.equals(chatPopup?.chat?.gameId)) {
chatPopup?.addMessage(incomingChatMsg.civName, incomingChatMsg.message)
chatPopup?.addMessage(
incomingChatMsg.civName,
incomingChatMsg.message,
toCivName = incomingChatMsg.toCivName,
)
}

if (chatPopup == null && incomingChatMsg.civName != "System") {
Expand Down
15 changes: 13 additions & 2 deletions core/src/com/unciv/logic/multiplayer/chat/ChatWebSocket.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ sealed class Message {
@Serializable
@SerialName("chat")
data class Chat(
val civName: String, val message: String, val gameId: String
val civName: String,
val message: String,
val gameId: String,
/** Multiplayer player UUID of the recipient; null = broadcast to the game. */
val toPlayerId: String? = null,
Comment thread
Fanfblrik marked this conversation as resolved.
Outdated
/** Display name of the recipient civ; null for broadcast. */
val toCivName: String? = null,
Comment thread
Fanfblrik marked this conversation as resolved.
Outdated
) : Message()

@Serializable
Expand All @@ -59,7 +65,11 @@ sealed class Response {
@Serializable
@SerialName("chat")
data class Chat(
val civName: String, val message: String, val gameId: String? = null
val civName: String,
val message: String,
val gameId: String? = null,
val toPlayerId: String? = null,
val toCivName: String? = null,
) : Response()

@Serializable
Expand Down Expand Up @@ -103,6 +113,7 @@ object ChatWebSocket {
// DO NOT OMIT
// if omitted the "type" field will be missing from all outgoing messages
classDiscriminatorMode = ClassDiscriminatorMode.ALL_JSON_OBJECTS
ignoreUnknownKeys = true
})
}
}
Expand Down
93 changes: 76 additions & 17 deletions core/src/com/unciv/ui/screens/worldscreen/chat/ChatPopup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import com.badlogic.gdx.scenes.scene2d.ui.Button
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.multiplayer.chat.Chat
import com.unciv.logic.multiplayer.chat.ChatStore
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.coerceLightnessAtLeast
import com.unciv.ui.components.extensions.setItems
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.widgets.UncivTextField
Expand All @@ -28,18 +30,29 @@ private val civChatColorsMap = mapOf<String, Color>(
"Server" to Color.DARK_GRAY,
)

private class ChatRecipient(
val displayName: String,
val playerId: String?,
val civName: String?,
) {
override fun toString() = displayName.tr()
}

class ChatPopup(
val chat: Chat,
private val worldScreen: WorldScreen,
) : Popup(screen = worldScreen, scrollable = Scrollability.None) {
companion object {
// the percentage of the minimum lightness allowed for a civName
const val CIVNAME_COLOR_MIN_LIGHTNESS = 0.55f
/** Server chat protocol version that supports private messages. */
const val PRIVATE_MESSAGE_CHAT_VERSION = 2
}

private val chatTable = Table(skin)
private val scrollPane = ScrollPane(chatTable, skin)
private val messageField = UncivTextField(hint = "Type something...")
private var recipientSelect: SelectBox<ChatRecipient>? = null

init {
ChatStore.chatPopup = this
Expand All @@ -49,6 +62,7 @@ class ChatPopup(
* Layout:
* | ChatHeader | CloseButton |
* | ChatTable (colSpan = 2) |
* | RecipientSelect (optional, colSpan = 2) |
* | MessageField | SendButton |
*/

Expand Down Expand Up @@ -77,6 +91,18 @@ class ChatPopup(
.size(0.5f * worldScreen.stage.width, 0.5f * worldScreen.stage.height)
.expand().fill().row()

if (supportsPrivateMessages()) {
val recipients = buildRecipientOptions()
if (recipients.size > 1) {
val select = SelectBox<ChatRecipient>(skin)
select.setItems(recipients)
select.selected = recipients.first()
recipientSelect = select
add("To:".toLabel()).left().padLeft(5f)
add(select).expandX().fillX().padBottom(5f).row()
}
}

// Input: | MessageField | SendButton |
add(messageField).expandX().fillX()
val sendButton = Button(skin)
Expand All @@ -99,35 +125,68 @@ class ChatPopup(
})
}

fun sendMessage() {
val message = messageField.text.trim()
private fun supportsPrivateMessages(): Boolean =
UncivGame.Current.onlineMultiplayer.multiplayerServer.getFeatureSet().chatVersion >=
PRIVATE_MESSAGE_CHAT_VERSION

private fun ownCivNameAndId(): Pair<String, String?> {
val userId = UncivGame.Current.settings.multiplayer.getUserId()
val currentPlayerCiv = worldScreen.gameInfo.currentPlayerCiv
val civName = if (currentPlayerCiv.playerId == userId) {
currentPlayerCiv.civID
} else {
// what do I do if someone is a spectator?
worldScreen.gameInfo.civilizations.firstOrNull { civ -> civ.playerId == userId }?.civID
?: "Unknown"
if (currentPlayerCiv.playerId == userId) {
return currentPlayerCiv.civID to userId
}
val ownCiv = worldScreen.gameInfo.civilizations.firstOrNull { civ -> civ.playerId == userId }
return (ownCiv?.civID ?: "Unknown") to ownCiv?.playerId
}

private fun buildRecipientOptions(): List<ChatRecipient> {
val (ownCivName, ownPlayerId) = ownCivNameAndId()
if (ownCivName == "Unknown" || ownPlayerId.isNullOrBlank()) {
return listOf(ChatRecipient("Everyone", null, null))
}

if (message.isNotEmpty()) {
chat.requestMessageSend(civName, message)
messageField.setText("")
val options = mutableListOf(ChatRecipient("Everyone", null, null))
for (civ in worldScreen.gameInfo.civilizations) {
if (!civ.isMajorCiv()) continue
if (!civ.isHuman()) continue
if (civ.playerId.isBlank()) continue
if (civ.playerId == ownPlayerId) continue
options.add(ChatRecipient(civ.civID, civ.playerId, civ.civID))
}
return options
}

fun sendMessage() {
val message = messageField.text.trim()
if (message.isEmpty()) return

val (civName, _) = ownCivNameAndId()
val recipient = recipientSelect?.selected
chat.requestMessageSend(
civName = civName,
message = message,
toPlayerId = recipient?.playerId,
toCivName = recipient?.civName,
)
messageField.setText("")
}

fun addMessage(
senderCivName: String,
message: String,
suffix: String? = null,
toCivName: String? = null,
scroll: Boolean = true
) {
val line = Label(
"${senderCivName.tr()}${if (suffix != null) " [${suffix.tr()}]" else ""}: ${message.tr()}",
skin
).apply {
val namePart = buildString {
append(senderCivName.tr())
if (suffix != null) append(" [${suffix.tr()}]")
if (toCivName != null) append(" → ${toCivName.tr()}")
append(": ")
append(message.tr())
}

val line = Label(namePart, skin).apply {
wrap = true

val civNameColor =
Expand All @@ -144,8 +203,8 @@ class ChatPopup(

private fun populateChat() {
chatTable.clearChildren()
chat.forEachMessage { civName, message ->
addMessage(civName, message)
chat.forEachMessage { entry ->
addMessage(entry.civName, entry.message, toCivName = entry.toCivName)
}
ChatStore.pollGlobalMessages { civName, message ->
addMessage(civName, message, suffix = "one time")
Expand Down
Loading
Loading