All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.robocode.tankroyale.gui.client.Client.kt Maven / Gradle / Ivy

package dev.robocode.tankroyale.gui.client

import dev.robocode.tankroyale.gui.client.ClientEvents.onBotListUpdate
import dev.robocode.tankroyale.gui.client.ClientEvents.onConnected
import dev.robocode.tankroyale.gui.client.ClientEvents.onGameAborted
import dev.robocode.tankroyale.gui.client.ClientEvents.onGameEnded
import dev.robocode.tankroyale.gui.client.ClientEvents.onGamePaused
import dev.robocode.tankroyale.gui.client.ClientEvents.onGameResumed
import dev.robocode.tankroyale.gui.client.ClientEvents.onGameStarted
import dev.robocode.tankroyale.gui.client.ClientEvents.onRoundEnded
import dev.robocode.tankroyale.gui.client.ClientEvents.onRoundStarted
import dev.robocode.tankroyale.gui.client.ClientEvents.onTickEvent
import dev.robocode.tankroyale.gui.model.*
import dev.robocode.tankroyale.gui.settings.ConfigSettings
import dev.robocode.tankroyale.gui.settings.GamesSettings
import dev.robocode.tankroyale.gui.settings.ServerSettings
import dev.robocode.tankroyale.gui.ui.server.ServerEvents
import dev.robocode.tankroyale.gui.ui.tps.TpsEvents
import dev.robocode.tankroyale.gui.util.Version
import kotlinx.serialization.PolymorphicSerializer
import java.net.URI
import java.util.concurrent.atomic.AtomicBoolean

object Client {

    var currentGameSetup: GameSetup? = null

    var currentTick: TickEvent? = null

    private val isRunning = AtomicBoolean(false)
    private val isPaused = AtomicBoolean(false)

    private var participants = listOf()
    private var bots = mutableSetOf()

    private var websocket: WebSocketClient? = null

    private val json = MessageConstants.json

    private var gameTypes = setOf()

    private lateinit var lastStartGame: StartGame

    private var lastTps: Int? = null

    private val savedStdOutput =
        mutableMapOf>>()
    private val savedStdError =
        mutableMapOf>>()

    init {
        TpsEvents.onTpsChanged.subscribe(Client) { changeTps(it.tps) }

        ServerEvents.onStopped.subscribe(Client) {
            isRunning.set(false)
            isPaused.set(false)

            bots.clear()
        }
    }

    private fun isConnected(): Boolean = websocket?.isOpen() ?: false

    fun connect() {
        check (!isConnected()) { "Websocket is already connected" }
        websocket = WebSocketClient(URI(ServerSettings.currentServerUrl))

        WebSocketClientEvents.apply {
            websocket?.let { ws ->
                onOpen.subscribe(ws) { onConnected.fire(Unit) }
                onMessage.subscribe(ws) { onMessage(it) }
                onError.subscribe(ws) {
                    System.err.println("WebSocket error: " + it.message)
                    it.printStackTrace()
                }
                try {
                    ws.open() // must be called AFTER onOpen.subscribe()
                } catch (_: Exception) {
                    // to prevent redundant subscriptions which are kept both on failure, and
                    // new attempt to open the web socket
                    onOpen.unsubscribe(ws)
                    onMessage.unsubscribe(ws)
                    onError.unsubscribe(ws)
                }
            }
        }
    }

    fun close() {
        stopGame()

        if (isConnected()) {
            WebSocketClientEvents.apply {
                websocket?.let { ws ->
                    onOpen.unsubscribe(ws)
                    onMessage.unsubscribe(ws)
                    onError.unsubscribe(ws)
                    ws.close()
                }
            }
            websocket = null
        }

        savedStdOutput.clear()
        savedStdError.clear()
    }

    fun startGame(botAddresses: Set) {
        savedStdOutput.clear()
        savedStdError.clear()

        if (isRunning.get()) {
            stopGame()
        }

        val displayName = ConfigSettings.gameType.displayName
        val gameSetup = GamesSettings.games[displayName]!!

        lastStartGame = StartGame(gameSetup.toGameSetup(), botAddresses)
        send(lastStartGame)
    }

    fun stopGame() {
        resumeGame()
        if (isRunning.get()) {
            send(StopGame)
        }
    }

    fun restartGame() {
        if (isRunning.get()) {
            val eventOwner = Object()
            onGameAborted.subscribe(eventOwner, true) {
                startWithLastGameSetup()
            }
            onGameEnded.subscribe(eventOwner, true) {
                startWithLastGameSetup()
            }
            stopGame()

        } else {
            startWithLastGameSetup()
        }
    }

    fun pauseGame() {
        if (isRunning.get() && !isPaused.get()) {
            send(PauseGame)
        }
    }

    fun resumeGame() {
        if (isRunning.get() && isPaused.get()) {
            send(ResumeGame)
        }
    }

    internal fun doNextTurn() {
        if (isRunning.get() && isPaused.get()) {
            send(NextTurn)
        }
    }

    fun isGameRunning(): Boolean = isRunning.get()
    fun isGamePaused(): Boolean = isPaused.get()

    val joinedBots: Set get() = bots

    fun getParticipant(botId: Int): Participant = participants.first { participant -> participant.id == botId }

    fun getStandardOutput(botId: Int): Map>? = savedStdOutput[botId]

    fun getStandardError(botId: Int): Map>? = savedStdError[botId]

    private fun startWithLastGameSetup() {
        send(lastStartGame)
    }

    private fun send(message: Message) {
        check(isConnected()) { "Websocket is not connected" }
        websocket?.send(message)
    }

    private fun changeTps(tps: Int) {
        if (isRunning.get() && tps != lastTps) {
            lastTps = tps
            send(ChangeTps(tps))
        }
    }

    private fun onMessage(msg: String) {
//        println("msg: $msg")
        when (val type = json.decodeFromString(PolymorphicSerializer(Message::class), msg)) {
            is TickEvent -> handleTickEvent(type)
            is ServerHandshake -> handleServerHandshake(type)
            is BotListUpdate -> handleBotListUpdate(type)
            is GameStartedEvent -> handleGameStarted(type)
            is GameEndedEvent -> handleGameEnded(type)
            is GameAbortedEvent -> handleGameAborted(type)
            is GamePausedEvent -> handleGamePaused(type)
            is GameResumedEvent -> handleGameResumed(type)
            is RoundStartedEvent -> handleRoundStarted(type)
            is RoundEndedEvent -> handleRoundEnded(type)
            is TpsChangedEvent -> {
                // do nothing to prevent TPS change loop between server and client
            }
            else -> throw IllegalArgumentException("Unknown content type: $type")
        }
    }

    private fun handleServerHandshake(serverHandshake: ServerHandshake) {
        gameTypes = serverHandshake.gameTypes

        val handshake = ControllerHandshake(
            sessionId = serverHandshake.sessionId,
            name = "Robocode Tank Royale UI",
            version = "${Version.version}",
            author = "Flemming N. Larsen",
            secret = ServerSettings.controllerSecrets.first()
        )
        send(handshake)
    }

    private fun handleBotListUpdate(botListUpdate: BotListUpdate) {
        bots = HashSet(botListUpdate.bots)
        onBotListUpdate.fire(botListUpdate)
    }

    private fun handleGameStarted(gameStartedEvent: GameStartedEvent) {
        isRunning.set(true)
        currentGameSetup = gameStartedEvent.gameSetup
        participants = gameStartedEvent.participants

        onGameStarted.fire(gameStartedEvent)

        changeTps(ConfigSettings.tps)
    }

    private fun handleGameEnded(gameEndedEvent: GameEndedEvent) {
        isRunning.set(false)
        isPaused.set(false)
        onGameEnded.fire(gameEndedEvent)
    }

    private fun handleGameAborted(gameAbortedEvent: GameAbortedEvent) {
        isRunning.set(false)
        isPaused.set(false)
        onGameAborted.fire(gameAbortedEvent)
    }

    private fun handleGamePaused(gamePausedEvent: GamePausedEvent) {
        isPaused.set(true)

        onGamePaused.fire(gamePausedEvent)
    }

    private fun handleGameResumed(gameResumedEvent: GameResumedEvent) {
        isPaused.set(false)
        onGameResumed.fire(gameResumedEvent)
    }

    private fun handleRoundStarted(roundStartedEvent: RoundStartedEvent) {
        onRoundStarted.fire(roundStartedEvent)
    }

    private fun handleRoundEnded(roundEndedEvent: RoundEndedEvent) {
        onRoundEnded.fire(roundEndedEvent)
    }

    private fun handleTickEvent(tickEvent: TickEvent) {
        currentTick = tickEvent

        onTickEvent.fire(tickEvent)

        updateSavedStdOutput(tickEvent)
    }

    private fun updateSavedStdOutput(tickEvent: TickEvent) {
        tickEvent.apply {
            botStates.forEach { botState ->
                val id = botState.id
                botState.stdOut?.let { updateStandardOutput(savedStdOutput, id, roundNumber, turnNumber, it) }
                botState.stdErr?.let { updateStandardOutput(savedStdError, id, roundNumber, turnNumber, it) }
            }
            ClientEvents.onStdOutputUpdated.fire(tickEvent)
        }
    }

    private fun updateStandardOutput(
        stdOutputMaps: MutableMap>>,
        id: Int, round: Int, turn: Int, output: String
    ) {
        stdOutputMaps
            .getOrPut(id) { LinkedHashMap() }
            .getOrPut(round) { LinkedHashMap() }[turn] = output
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy