diff --git a/core/src/com/unciv/logic/AlternatingStateManager.kt b/core/src/com/unciv/logic/AlternatingStateManager.kt new file mode 100644 index 0000000000..707186035c --- /dev/null +++ b/core/src/com/unciv/logic/AlternatingStateManager.kt @@ -0,0 +1,52 @@ +package com.unciv.logic + +import com.badlogic.gdx.Gdx +import com.unciv.utils.Concurrency +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlin.time.Clock +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlin.time.ExperimentalTime + +class AlternatingStateManager( + private val name: String, + private val originalState: () -> Unit, + private val alternateState: () -> Unit +) { + private var job: Job? = null + + @OptIn(ExperimentalTime::class) + fun start( + duration: Duration = 5.seconds, interval: Duration = 500.milliseconds + ) { + job?.cancel() + job = Concurrency.run(name) { + val startTime = Clock.System.now() + + var isAlternateState = true + while (true) { + if (isAlternateState) { + Gdx.app.postRunnable(alternateState) + isAlternateState = false + } else { + Gdx.app.postRunnable(originalState) + isAlternateState = true + } + + if (Clock.System.now() - startTime >= duration) { + Gdx.app.postRunnable(originalState) + return@run + } + + delay(interval) + } + } + } + + fun stop() { + job?.cancel() + Gdx.app.postRunnable(originalState) + } +} diff --git a/core/src/com/unciv/logic/multiplayer/chat/ChatStore.kt b/core/src/com/unciv/logic/multiplayer/chat/ChatStore.kt index 910df82725..f7b5fc10b7 100644 --- a/core/src/com/unciv/logic/multiplayer/chat/ChatStore.kt +++ b/core/src/com/unciv/logic/multiplayer/chat/ChatStore.kt @@ -1,7 +1,9 @@ package com.unciv.logic.multiplayer.chat import com.badlogic.gdx.Gdx +import com.unciv.UncivGame import com.unciv.ui.screens.worldscreen.chat.ChatPopup +import com.unciv.utils.toUUIDOrNull import java.util.Collections.synchronizedMap import java.util.LinkedList import java.util.Queue @@ -10,6 +12,8 @@ import java.util.UUID data class Chat( val gameId: UUID, ) { + var unreadCount = 0 + // pairs private val messages: MutableList> = mutableListOf(INITIAL_MESSAGE) @@ -52,6 +56,8 @@ object ChatStore { */ var chatPopup: ChatPopup? = null + var hasGlobalMessage = false + private var gameIdToChat: MutableMap = synchronizedMap(mutableMapOf()) /** When no [ChatPopup] is open to receive these oddities, we keep them here. @@ -72,21 +78,36 @@ object ChatStore { globalMessages = LinkedList() } - fun relayChatMessage(chat: Response.Chat) { + fun relayChatMessage(incomingChatMsg: Response.Chat) { Gdx.app.postRunnable { - if (chat.gameId == null || chat.gameId.isBlank()) { - relayGlobalMessage(chat.message, chat.civName) + if (incomingChatMsg.gameId == null || incomingChatMsg.gameId.isBlank()) { + relayGlobalMessage(incomingChatMsg.message, incomingChatMsg.civName) } else { val gameId = try { - UUID.fromString(chat.gameId) + UUID.fromString(incomingChatMsg.gameId) } catch (_: Throwable) { // Discard messages with invalid UUID return@postRunnable } - getChatByGameId(gameId).addMessage(chat.civName, chat.message) - if (chatPopup?.chat?.gameId == gameId) { - chatPopup?.addMessage(chat.civName, chat.message) + val chat = chatPopup?.chat ?: getChatByGameId(gameId) + chat.addMessage(incomingChatMsg.civName, incomingChatMsg.message) + if (gameId.equals(chatPopup?.chat?.gameId)) { + chatPopup?.addMessage(incomingChatMsg.civName, incomingChatMsg.message) + } + + if (chatPopup == null && incomingChatMsg.civName != "System") { + if (gameId.equals(UncivGame.Current.worldScreen?.gameInfo?.gameId?.toUUIDOrNull())) { + // ensures that you are not getting notified for your own messages + if (UncivGame.Current.worldScreen?.gameInfo?.currentPlayer != incomingChatMsg.civName) { + chat.unreadCount++ + UncivGame.Current.worldScreen?.chatButton?.triggerChatIndication() + } + } else { + // user is out of world screen or + // some other game not currently on screen has a message + chat.unreadCount++ + } } } } @@ -103,7 +124,13 @@ object ChatStore { fun relayGlobalMessage(message: String, civName: String = "System") { Gdx.app.postRunnable { - chatPopup?.addMessage(civName, message, suffix = "one time") ?: globalMessages.add(Pair(civName, message)) + if (civName != "System") { + hasGlobalMessage = chatPopup == null + if (hasGlobalMessage) UncivGame.Current.worldScreen?.chatButton?.triggerChatIndication() + } + + chatPopup?.addMessage(civName, message, suffix = "one time") + ?: globalMessages.add(Pair(civName, message)) } } } diff --git a/core/src/com/unciv/ui/images/IconTextButton.kt b/core/src/com/unciv/ui/images/IconTextButton.kt index ba0f8e0ed6..5a7a8921a3 100644 --- a/core/src/com/unciv/ui/images/IconTextButton.kt +++ b/core/src/com/unciv/ui/images/IconTextButton.kt @@ -7,8 +7,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.Cell import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.utils.Align import com.unciv.Constants -import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.components.extensions.toLabel +import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.screens.basescreen.BaseScreen /** @@ -23,10 +23,11 @@ open class IconTextButton( text: String, val icon: Actor? = null, fontSize: Int = Constants.defaultFontSize, - fontColor: Color = Color.WHITE -): Button(BaseScreen.skin) { + val fontColor: Color = Color.WHITE +) : Button(BaseScreen.skin) { /** [Label] instance produced by, and with content and formatting as specified in [String.toLabel]. */ val label = text.toLabel(fontColor, fontSize, hideIcons = true) // Since by definition we already have an icon + /** Table cell containing the [icon] if any, or `null` (that is, when no [icon] was supplied, the Cell will exist but have no Actor). */ val iconCell: Cell = if (icon != null) { @@ -37,6 +38,7 @@ open class IconTextButton( } else { add().padRight(fontSize / 2f) } + /** Table cell instance containing the [label]. */ val labelCell: Cell