Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
dev.robocode.tankroyale.server.connection.ClientWebSocketsHandler.kt Maven / Gradle / Ivy
package dev.robocode.tankroyale.server.connection
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import dev.robocode.tankroyale.schema.game.*
import dev.robocode.tankroyale.server.core.ServerSetup
import dev.robocode.tankroyale.server.dev.robocode.tankroyale.server.connection.IClientWebSocketObserver
import dev.robocode.tankroyale.server.dev.robocode.tankroyale.server.connection.IConnectionListener
import dev.robocode.tankroyale.server.dev.robocode.tankroyale.server.core.StatusCode
import dev.robocode.tankroyale.server.dev.robocode.tankroyale.server.util.VersionFileProvider
import org.java_websocket.WebSocket
import org.java_websocket.exceptions.WebsocketNotConnectedException
import org.java_websocket.handshake.ClientHandshake
import org.slf4j.LoggerFactory
import java.io.Closeable
import java.nio.ByteBuffer
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class ClientWebSocketsHandler(
private val setup: ServerSetup,
private val listener: IConnectionListener,
private val controllerSecrets: Set,
private val botSecrets: Set,
private val broadcastFunction: (clientSockets: Collection, message: String) -> Unit
) : IClientWebSocketObserver, Closeable {
companion object {
private const val MISSING_SESSION_ID = "Missing session id"
private const val INVALID_SECRET = "Invalid secret"
}
private val log = LoggerFactory.getLogger(this::class.java)
private val allSockets = ConcurrentHashMap.newKeySet()
private val botSockets = ConcurrentHashMap.newKeySet()
private val observerSockets = ConcurrentHashMap.newKeySet()
private val controllerSockets = ConcurrentHashMap.newKeySet()
private val sessionIds = ConcurrentHashMap ()
private val botHandshakes = ConcurrentHashMap()
private val observerHandshakes = ConcurrentHashMap()
private val controllerHandshakes = ConcurrentHashMap()
private val executorService = Executors.newCachedThreadPool()
private val gson = Gson()
private var currentGameSetup: GameSetup? = null
override fun close() {
shutdownAndAwaitTermination(executorService)
}
override fun onOpen(clientSocket: WebSocket, handshake: ClientHandshake) {
addSocketAndSendServerHandshake(clientSocket)
}
override fun onClose(clientSocket: WebSocket, code: Int, reason: String, remote: Boolean) {
removeSocket(clientSocket)
}
override fun onMessage(clientSocket: WebSocket, message: String) {
processMessage(clientSocket, message)
}
override fun onError(clientSocket: WebSocket, exception: Exception) {
handleException(clientSocket, exception)
}
private fun addSocketAndSendServerHandshake(clientSocket: WebSocket) {
allSockets += clientSocket
ServerHandshake().apply {
type = Message.Type.SERVER_HANDSHAKE
name = "Robocode Tank Royale server"
sessionId = generateAndStoreSessionId(clientSocket)
variant = "Tank Royale"
version = VersionFileProvider.version
gameTypes = setup.gameTypes
gameSetup = currentGameSetup
}.also {
send(clientSocket, Gson().toJson(it))
}
}
private fun removeSocket(clientSocket: WebSocket) {
closeSocket(clientSocket)
}
private fun processMessage(clientSocket: WebSocket, message: String) {
executorService.submit {
try {
gson.fromJson(message, JsonObject::class.java)["type"]?.let { jsonType ->
try {
val type = Message.Type.fromValue(jsonType.asString)
log.debug("Handling message: {}", type)
when (type) {
Message.Type.BOT_INTENT -> handleIntent(clientSocket, message)
Message.Type.BOT_HANDSHAKE -> handleBotHandshake(clientSocket, message)
Message.Type.OBSERVER_HANDSHAKE -> handleObserverHandshake(clientSocket, message)
Message.Type.CONTROLLER_HANDSHAKE -> handleControllerHandshake(clientSocket, message)
Message.Type.BOT_READY -> handleBotReady(clientSocket)
Message.Type.START_GAME -> handleStartGame(message)
Message.Type.STOP_GAME -> handleStopGame()
Message.Type.PAUSE_GAME -> handlePauseGame()
Message.Type.RESUME_GAME -> handleResumeGame()
Message.Type.NEXT_TURN -> handleNextTurn()
Message.Type.CHANGE_TPS -> handleChangeTps(message)
else -> handleException(
clientSocket,
IllegalStateException("Unhandled message type: $type")
)
}
} catch (ex: IllegalArgumentException) {
handleException(
clientSocket,
IllegalStateException("Unhandled message type: ${jsonType.asString}")
)
}
}
} catch (exception: JsonSyntaxException) {
log.error("Invalid message: $message", exception)
} catch (exception: Exception) {
log.error("Error when passing message: $message", exception)
}
}
}
fun getBotSockets(): Set = botSockets.toSet()
fun getObserverAndControllerSockets(): Set = observerSockets.union(controllerSockets)
fun getBotHandshakes(): Map = botHandshakes
private fun shutdownAndAwaitTermination(pool: ExecutorService) {
pool.apply {
shutdown() // Disable new tasks from being submitted
try {
if (!awaitTermination(5, TimeUnit.SECONDS)) {
shutdownNow()
if (!awaitTermination(5, TimeUnit.SECONDS)) {
log.warn("Pool did not terminate")
}
}
} catch (ex: InterruptedException) {
shutdownNow()
Thread.currentThread().interrupt()
}
}
}
override fun send(clientSocket: WebSocket, message: String) {
log.debug("Send to: client: {}, message: {}", clientSocket.remoteSocketAddress, message)
executorService.submit {
try {
clientSocket.send(message)
} catch (e: WebsocketNotConnectedException) {
closeSocket(clientSocket)
}
}
}
override fun broadcast(clientSockets: Collection, message: String) {
log.debug("Broadcast to clients: message: {}", message)
executorService.submit {
broadcastFunction(clientSockets, message)
}
}
private fun closeSocket(clientSocket: WebSocket) {
allSockets -= clientSocket
when {
botSockets.remove(clientSocket) -> handleBotLeft(clientSocket)
observerSockets.remove(clientSocket) -> handleObserverLeft(clientSocket)
controllerSockets.remove(clientSocket) -> handleControllerLeft(clientSocket)
}
sessionIds.remove(clientSocket)
}
private fun handleBotLeft(clientSocket: WebSocket) {
botHandshakes[clientSocket]?.let {
listener.onBotLeft(clientSocket, it)
}
botHandshakes -= clientSocket
}
private fun handleObserverLeft(clientSocket: WebSocket) {
observerHandshakes[clientSocket]?.let {
listener.onObserverLeft(clientSocket, it)
}
observerHandshakes -= clientSocket
}
private fun handleControllerLeft(clientSocket: WebSocket) {
controllerHandshakes[clientSocket]?.let {
listener.onControllerLeft(clientSocket, it)
}
controllerHandshakes -= clientSocket
}
private fun generateAndStoreSessionId(clientSocket: WebSocket): String {
val sessionId = generateSessionId()
check(!sessionIds.values.contains(sessionId)) {
"Generated session id has been generated before. It must be unique"
}
sessionIds[clientSocket] = sessionId
return sessionId
}
private fun generateSessionId(): String {
val uuid = UUID.randomUUID()
val byteBuffer = ByteBuffer.wrap(ByteArray(16))
byteBuffer.putLong(uuid.mostSignificantBits)
byteBuffer.putLong(uuid.leastSignificantBits)
return Base64.getEncoder().withoutPadding().encodeToString(byteBuffer.array())
}
private fun handleIntent(clientSocket: WebSocket, message: String) {
botHandshakes[clientSocket]?.let { botHandshake ->
val intent = gson.fromJson(message, BotIntent::class.java)
listener.onBotIntent(clientSocket, botHandshake, intent)
}
}
private fun handleBotHandshake(clientSocket: WebSocket, message: String) {
gson.fromJson(message, BotHandshake::class.java).apply {
if (sessionId.isNullOrBlank() || !sessionIds.values.contains(sessionId)) {
log.info("Ignoring bot missing session id: $name, version: $version")
clientSocket.close(StatusCode.POLICY_VIOLATION.value, MISSING_SESSION_ID)
} else if (botSecrets.isNotEmpty() && !botSecrets.contains(secret)) {
log.info("Ignoring bot using invalid secret: $name, version: $version")
clientSocket.close(StatusCode.POLICY_VIOLATION.value, INVALID_SECRET)
} else {
botSockets += clientSocket
botHandshakes[clientSocket] = this
listener.onBotJoined(clientSocket, this)
}
}
}
private fun handleObserverHandshake(clientSocket: WebSocket, message: String) {
gson.fromJson(message, ObserverHandshake::class.java).apply {
if (sessionId.isNullOrBlank() || !sessionIds.values.contains(sessionId)) {
log.info("Ignoring observer missing session id: $name, version: $version")
clientSocket.close(StatusCode.POLICY_VIOLATION.value, MISSING_SESSION_ID)
} else if (controllerSecrets.isNotEmpty() && !controllerSecrets.contains(secret)) {
log.info("Ignoring observer using invalid secret: name: $name, version: $version")
clientSocket.close(StatusCode.POLICY_VIOLATION.value, INVALID_SECRET)
} else {
observerSockets += clientSocket
observerHandshakes[clientSocket] = this
listener.onObserverJoined(clientSocket, this)
}
}
}
private fun handleControllerHandshake(clientSocket: WebSocket, message: String) {
gson.fromJson(message, ControllerHandshake::class.java).apply {
if (sessionId.isNullOrBlank() || !sessionIds.values.contains(sessionId)) {
log.info("Ignoring controller missing session id: $name, version: $version")
clientSocket.close(StatusCode.POLICY_VIOLATION.value, MISSING_SESSION_ID)
} else if (controllerSecrets.isNotEmpty() && !controllerSecrets.contains(secret)) {
log.info("Ignoring controller using invalid secret: name: $name, version: $version")
clientSocket.close(StatusCode.POLICY_VIOLATION.value, INVALID_SECRET)
} else {
controllerSockets += clientSocket
controllerHandshakes[clientSocket] = this
listener.onControllerJoined(clientSocket, this)
}
}
}
private fun handleBotReady(clientSocket: WebSocket) {
botHandshakes[clientSocket]?.let { botHandshake ->
listener.onBotReady(clientSocket, botHandshake)
}
}
private fun handleStartGame(message: String) {
gson.fromJson(message, StartGame::class.java).apply {
currentGameSetup = gameSetup
listener.onStartGame(gameSetup, botAddresses.toSet())
}
}
private fun handleStopGame() {
executorService.submit(listener::onAbortGame)
}
private fun handlePauseGame() {
executorService.submit(listener::onPauseGame)
}
private fun handleResumeGame() {
executorService.submit(listener::onResumeGame)
}
private fun handleNextTurn() {
executorService.submit(listener::onNextTurn)
}
private fun handleChangeTps(message: String) {
executorService.submit {
gson.fromJson(message, ChangeTps::class.java).apply {
listener.onChangeTps(tps)
}
}
}
private fun handleException(clientSocket: WebSocket, exception: Exception) {
log.error("Error: client: {}, message: {}", clientSocket.remoteSocketAddress, exception.message)
listener.onException(clientSocket, exception)
}
}