feat(websocket)!: Implement WebSocket connection and event handling
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
package actor
|
||||
|
||||
import org.apache.pekko.actor.{Actor, ActorRef}
|
||||
import org.apache.pekko.http.scaladsl.model.ContentRange.Other
|
||||
|
||||
|
||||
class KnockOutWebSocketActor(
|
||||
out: ActorRef,
|
||||
) extends Actor {
|
||||
def receive: Receive = {
|
||||
case msg: String =>
|
||||
out ! s"Received your message: ${msg}"
|
||||
case other: Other =>
|
||||
println(s"Received unknown message: $other")
|
||||
}
|
||||
|
||||
def sendJsonToClient(json: String): Unit = {
|
||||
println("Received event from Controller")
|
||||
out ! json
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import scala.util.Try
|
||||
@Singleton
|
||||
class IngameController @Inject() (
|
||||
val cc: ControllerComponents,
|
||||
val podManager: PodManager,
|
||||
val authAction: AuthAction,
|
||||
implicit val ec: ExecutionContext
|
||||
) extends AbstractController(cc) {
|
||||
@@ -54,7 +53,7 @@ class IngameController @Inject() (
|
||||
}
|
||||
|
||||
def game(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
val results = Try {
|
||||
@@ -70,7 +69,7 @@ class IngameController @Inject() (
|
||||
}
|
||||
}
|
||||
def startGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
val game = PodManager.getGame(gameId)
|
||||
val result = Try {
|
||||
game match {
|
||||
case Some(g) =>
|
||||
@@ -111,7 +110,7 @@ class IngameController @Inject() (
|
||||
}
|
||||
}
|
||||
def kickPlayer(gameId: String, playerToKick: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
val game = PodManager.getGame(gameId)
|
||||
val playerToKickUUID = UUID.fromString(playerToKick)
|
||||
val result = Try {
|
||||
game.get.leaveGame(playerToKickUUID)
|
||||
@@ -129,7 +128,7 @@ class IngameController @Inject() (
|
||||
}
|
||||
}
|
||||
def leaveGame(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
val game = PodManager.getGame(gameId)
|
||||
val result = Try {
|
||||
game.get.leaveGame(request.user.id)
|
||||
}
|
||||
@@ -147,7 +146,7 @@ class IngameController @Inject() (
|
||||
}
|
||||
|
||||
def playCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
||||
val game = podManager.getGame(gameId)
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
val jsonBody = request.body.asJson
|
||||
@@ -218,7 +217,7 @@ class IngameController @Inject() (
|
||||
}
|
||||
}
|
||||
def playDogCard(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] => {
|
||||
val game = podManager.getGame(gameId)
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) => {
|
||||
val jsonBody = request.body.asJson
|
||||
@@ -282,7 +281,7 @@ class IngameController @Inject() (
|
||||
}
|
||||
}
|
||||
def playTrump(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
val jsonBody = request.body.asJson
|
||||
@@ -334,7 +333,7 @@ class IngameController @Inject() (
|
||||
}
|
||||
}
|
||||
def playTie(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
val jsonBody = request.body.asJson
|
||||
@@ -388,7 +387,7 @@ class IngameController @Inject() (
|
||||
|
||||
|
||||
def returnToLobby(gameId: String): Action[AnyContent] = authAction { implicit request: AuthenticatedRequest[AnyContent] =>
|
||||
val game = podManager.getGame(gameId)
|
||||
val game = PodManager.getGame(gameId)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
val result = Try {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package controllers
|
||||
|
||||
import auth.{AuthAction, AuthenticatedRequest}
|
||||
import logic.PodManager
|
||||
import auth.AuthAction
|
||||
import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents}
|
||||
import play.api.routing.JavaScriptReverseRouter
|
||||
|
||||
@@ -10,7 +9,6 @@ import javax.inject.Inject
|
||||
class JavaScriptRoutingController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val authAction: AuthAction,
|
||||
val podManager: PodManager
|
||||
) extends BaseController {
|
||||
def javascriptRoutes(): Action[AnyContent] =
|
||||
Action { implicit request =>
|
||||
|
||||
@@ -17,7 +17,6 @@ import javax.inject.*
|
||||
class MainMenuController @Inject()(
|
||||
val controllerComponents: ControllerComponents,
|
||||
val authAction: AuthAction,
|
||||
val podManager: PodManager,
|
||||
val ingameController: IngameController
|
||||
) extends BaseController {
|
||||
|
||||
@@ -39,7 +38,7 @@ class MainMenuController @Inject()(
|
||||
val playeramount: String = (jsonBody.get \ "playeramount").asOpt[String]
|
||||
.getOrElse(throw new IllegalArgumentException("Player amount is required."))
|
||||
|
||||
val gameLobby = podManager.createGame(
|
||||
val gameLobby = PodManager.createGame(
|
||||
host = request.user,
|
||||
name = gamename,
|
||||
maxPlayers = playeramount.toInt
|
||||
@@ -64,7 +63,7 @@ class MainMenuController @Inject()(
|
||||
(jsValue \ "gameId").asOpt[String]
|
||||
}
|
||||
if (gameId.isDefined) {
|
||||
val game = podManager.getGame(gameId.get)
|
||||
val game = PodManager.getGame(gameId.get)
|
||||
game match {
|
||||
case Some(g) =>
|
||||
g.addUser(request.user)
|
||||
|
||||
@@ -24,7 +24,6 @@ object PollingController {
|
||||
@Singleton
|
||||
class PollingController @Inject() (
|
||||
val cc: ControllerComponents,
|
||||
val podManager: PodManager,
|
||||
val authAction: AuthAction,
|
||||
val ingameController: IngameController,
|
||||
implicit val ec: ExecutionContext
|
||||
@@ -117,7 +116,7 @@ class PollingController @Inject() (
|
||||
|
||||
val playerId = request.user.id
|
||||
|
||||
podManager.getGame(gameId) match {
|
||||
PodManager.getGame(gameId) match {
|
||||
case Some(game) =>
|
||||
val playerEventQueue = game.getEventsOfPlayer(playerId)
|
||||
if (playerEventQueue.nonEmpty) {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package controllers
|
||||
import actor.KnockOutWebSocketActor
|
||||
|
||||
|
||||
import auth.AuthAction
|
||||
import logic.PodManager
|
||||
import logic.user.SessionManager
|
||||
import model.sessions.{UserSession, UserWebsocketActor}
|
||||
import org.apache.pekko.actor.{ActorRef, ActorSystem, Props}
|
||||
import org.apache.pekko.stream.Materializer
|
||||
import play.api.*
|
||||
@@ -12,17 +17,28 @@ import javax.inject.*
|
||||
@Singleton
|
||||
class WebsocketController @Inject()(
|
||||
cc: ControllerComponents,
|
||||
val sessionManger: SessionManager,
|
||||
)(implicit system: ActorSystem, mat: Materializer) extends AbstractController(cc) {
|
||||
|
||||
object KnockOutWebSocketActorFactory {
|
||||
def create(out: ActorRef) = {
|
||||
Props(new KnockOutWebSocketActor(out))
|
||||
def create(out: ActorRef, userSession: UserSession): Props = {
|
||||
Props(new UserWebsocketActor(out, userSession))
|
||||
}
|
||||
}
|
||||
def socket() = WebSocket.accept[String, String] { request =>
|
||||
|
||||
|
||||
def socket(): WebSocket = WebSocket.accept[String, String] { request =>
|
||||
val session = request.cookies.get("sessionId")
|
||||
if (session.isEmpty) throw new Exception("No session cookie found")
|
||||
val userOpt = sessionManger.getUserBySession(session.get.value)
|
||||
if (userOpt.isEmpty) throw new Exception("Invalid session")
|
||||
val user = userOpt.get
|
||||
val game = PodManager.identifyGameOfUser(user)
|
||||
if (game.isEmpty) throw new Exception("User is not in a game")
|
||||
val userSession = game.get.getUserSession(user.id)
|
||||
ActorFlow.actorRef { out =>
|
||||
println("Connect received")
|
||||
KnockOutWebSocketActorFactory.create(out)
|
||||
KnockOutWebSocketActorFactory.create(out, userSession)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ import util.GameUtil
|
||||
import javax.inject.Singleton
|
||||
import scala.collection.mutable
|
||||
|
||||
@Singleton
|
||||
class PodManager {
|
||||
object PodManager {
|
||||
|
||||
val TTL: Long = System.currentTimeMillis() + 86400000L // 24 hours in milliseconds
|
||||
val podIp: String = System.getenv("POD_IP")
|
||||
val podName: String = System.getenv("POD_NAME")
|
||||
|
||||
private val sessions: mutable.Map[String, GameLobby] = mutable.Map()
|
||||
private val userSession: mutable.Map[User, String] = mutable.Map()
|
||||
private val injector: Injector = Guice.createInjector(KnockOutWebConfigurationModule())
|
||||
|
||||
def createGame(
|
||||
@@ -35,6 +35,7 @@ class PodManager {
|
||||
host = host
|
||||
)
|
||||
sessions += (gameLobby.id -> gameLobby)
|
||||
userSession += (host -> gameLobby.id)
|
||||
gameLobby
|
||||
}
|
||||
|
||||
@@ -44,6 +45,30 @@ class PodManager {
|
||||
|
||||
private[logic] def removeGame(gameId: String): Unit = {
|
||||
sessions.remove(gameId)
|
||||
// Also remove all user sessions associated with this game
|
||||
userSession.filterInPlace((_, v) => v != gameId)
|
||||
}
|
||||
|
||||
def registerUserToGame(user: User, gameId: String): Boolean = {
|
||||
if (sessions.contains(gameId)) {
|
||||
userSession += (user -> gameId)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def unregisterUserFromGame(user: User): Unit = {
|
||||
userSession.remove(user)
|
||||
}
|
||||
|
||||
def identifyGameOfUser(user: User): Option[GameLobby] = {
|
||||
userSession.get(user) match {
|
||||
case Some(gameId) => sessions.get(gameId)
|
||||
case None => None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
|
||||
import de.knockoutwhist.rounds.{Match, Round, Trick}
|
||||
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
|
||||
import exceptions.*
|
||||
import logic.PodManager
|
||||
import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, NewTrick, ReloadEvent}
|
||||
import model.sessions.{InteractionType, UserSession}
|
||||
import model.users.User
|
||||
@@ -69,9 +70,11 @@ class GameLobby private(
|
||||
if (logic.getCurrentState != Lobby) throw new IllegalStateException("The game has already started!")
|
||||
val userSession = new UserSession(
|
||||
user = user,
|
||||
host = false
|
||||
host = false,
|
||||
gameLobby = this
|
||||
)
|
||||
users += (user.id -> userSession)
|
||||
PodManager.registerUserToGame(user, id)
|
||||
addToQueue(LobbyUpdate)
|
||||
userSession
|
||||
}
|
||||
@@ -156,7 +159,14 @@ class GameLobby private(
|
||||
if (sessionOpt.isEmpty) {
|
||||
throw new NotInThisGameException("You are not in this game!")
|
||||
}
|
||||
if (sessionOpt.get.host) {
|
||||
logic.invoke(SessionClosed())
|
||||
users.clear()
|
||||
PodManager.removeGame(id)
|
||||
return
|
||||
}
|
||||
users.remove(userId)
|
||||
PodManager.unregisterUserFromGame(sessionOpt.get.user)
|
||||
addToQueue(LobbyUpdate)
|
||||
}
|
||||
|
||||
@@ -338,7 +348,8 @@ object GameLobby {
|
||||
)
|
||||
lobby.users += (host.id -> new UserSession(
|
||||
user = host,
|
||||
host = true
|
||||
host = true,
|
||||
gameLobby = lobby
|
||||
))
|
||||
lobby
|
||||
}
|
||||
|
||||
@@ -2,13 +2,19 @@ package model.sessions
|
||||
|
||||
import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import logic.game.GameLobby
|
||||
import model.users.User
|
||||
import org.apache.pekko.actor.{Actor, ActorRef}
|
||||
import play.api.libs.json.{JsObject, JsValue, Json}
|
||||
import util.WebsocketEventMapper
|
||||
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.{Lock, ReentrantLock}
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
class UserSession(val user: User, val host: Boolean) extends PlayerSession {
|
||||
class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) extends PlayerSession {
|
||||
var canInteract: Option[InteractionType] = None
|
||||
var websocketActor: Option[UserWebsocketActor] = None
|
||||
val lock: ReentrantLock = ReentrantLock()
|
||||
|
||||
override def updatePlayer(event: SimpleEvent): Unit = {
|
||||
@@ -31,5 +37,17 @@ class UserSession(val user: User, val host: Boolean) extends PlayerSession {
|
||||
def resetCanInteract(): Unit = {
|
||||
canInteract = None
|
||||
}
|
||||
|
||||
def handleWebResponse(eventType: String, data: JsObject): Unit = {
|
||||
lock.lock()
|
||||
Try {
|
||||
eventType match {
|
||||
case "Ping" =>
|
||||
// No action needed for Ping
|
||||
()
|
||||
}
|
||||
}
|
||||
lock.unlock()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
94
knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala
Normal file
94
knockoutwhistweb/app/model/sessions/UserWebsocketActor.scala
Normal file
@@ -0,0 +1,94 @@
|
||||
package model.sessions
|
||||
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import org.apache.pekko.actor.{Actor, ActorRef}
|
||||
import play.api.libs.json.{JsObject, JsValue, Json}
|
||||
import util.WebsocketEventMapper
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
class UserWebsocketActor(
|
||||
out: ActorRef,
|
||||
session: UserSession
|
||||
) extends Actor {
|
||||
|
||||
if (session.websocketActor.isDefined) {
|
||||
session.websocketActor.foreach(actor => actor.transmitTextToClient("Error: Multiple websocket connections detected. Closing this connection."))
|
||||
context.stop(self)
|
||||
} else {
|
||||
session.websocketActor = Some(this)
|
||||
}
|
||||
|
||||
override def receive: Receive = {
|
||||
case msg: String =>
|
||||
val jsonObject = Try {
|
||||
Json.parse(msg)
|
||||
}
|
||||
Try {
|
||||
jsonObject match {
|
||||
case Success(value) =>
|
||||
handle(value)
|
||||
case Failure(exception) =>
|
||||
transmitTextToClient(s"Error parsing JSON: ${exception.getMessage}")
|
||||
}
|
||||
}.failed.foreach(
|
||||
ex => transmitTextToClient(s"Error handling message: ${ex.getMessage}")
|
||||
)
|
||||
case other =>
|
||||
}
|
||||
|
||||
def transmitEventToClient(event: SimpleEvent): Unit = {
|
||||
val jsonString = WebsocketEventMapper.toJsonString(event)
|
||||
out ! jsonString
|
||||
}
|
||||
|
||||
private def transmitJsonToClient(jsonObj: JsObject): Unit = {
|
||||
out ! jsonObj.toString()
|
||||
}
|
||||
|
||||
private def transmitTextToClient(text: String): Unit = {
|
||||
out ! text
|
||||
}
|
||||
|
||||
private def handle(json: JsValue): Unit = {
|
||||
val idOpt = (json \ "id").asOpt[String]
|
||||
if (idOpt.isEmpty) {
|
||||
transmitJsonToClient(Json.obj(
|
||||
"status" -> "error",
|
||||
"error" -> "Missing 'id' field"
|
||||
))
|
||||
return
|
||||
}
|
||||
val id = idOpt.get
|
||||
val eventOpt = (json \ "event").asOpt[String]
|
||||
if (eventOpt.isEmpty) {
|
||||
transmitJsonToClient(Json.obj(
|
||||
"id" -> id,
|
||||
"event" -> null,
|
||||
"status" -> "error",
|
||||
"error" -> "Missing 'event' field"
|
||||
))
|
||||
return
|
||||
}
|
||||
val event = eventOpt.get
|
||||
val data = (json \ "data").asOpt[JsObject].getOrElse(Json.obj())
|
||||
val result = Try {
|
||||
session.handleWebResponse(event, data)
|
||||
}
|
||||
if (result.isSuccess) {
|
||||
transmitJsonToClient(Json.obj(
|
||||
"id" -> id,
|
||||
"event" -> event,
|
||||
"status" -> "success"
|
||||
))
|
||||
} else {
|
||||
transmitJsonToClient(Json.obj(
|
||||
"id" -> id,
|
||||
"event" -> event,
|
||||
"status" -> "error",
|
||||
"error" -> result.failed.get.getMessage
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
20
knockoutwhistweb/app/util/WebsocketEventMapper.scala
Normal file
20
knockoutwhistweb/app/util/WebsocketEventMapper.scala
Normal file
@@ -0,0 +1,20 @@
|
||||
package util
|
||||
|
||||
import de.knockoutwhist.utils.events.SimpleEvent
|
||||
import tools.jackson.databind.json.JsonMapper
|
||||
import tools.jackson.module.scala.ScalaModule
|
||||
|
||||
object WebsocketEventMapper {
|
||||
|
||||
private val scalaModule = ScalaModule.builder()
|
||||
.addAllBuiltinModules()
|
||||
.supportScala3Classes(true)
|
||||
.build()
|
||||
|
||||
private val mapper = JsonMapper.builder().addModule(scalaModule).build()
|
||||
|
||||
def toJsonString(obj: SimpleEvent): String = {
|
||||
mapper.writeValueAsString(obj)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,4 +34,5 @@
|
||||
});
|
||||
}
|
||||
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
|
||||
connectWebSocket()
|
||||
</script>
|
||||
@@ -104,4 +104,5 @@
|
||||
});
|
||||
}
|
||||
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
|
||||
connectWebSocket()
|
||||
</script>
|
||||
|
||||
@@ -68,4 +68,5 @@
|
||||
});
|
||||
}
|
||||
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
|
||||
connectWebSocket()
|
||||
</script>
|
||||
@@ -114,4 +114,5 @@
|
||||
});
|
||||
}
|
||||
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
|
||||
connectWebSocket()
|
||||
</script>
|
||||
@@ -78,4 +78,5 @@
|
||||
});
|
||||
}
|
||||
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
|
||||
connectWebSocket()
|
||||
</script>
|
||||
@@ -35,6 +35,7 @@
|
||||
particlesJS.load('particles-js', 'assets/conf/particlesjs-config.json', function() {
|
||||
console.log('callback - particles.js config loaded');
|
||||
});
|
||||
disconnectWebSocket();
|
||||
</script>
|
||||
<div id="particles-js" style="background-color: rgb(11, 8, 8);
|
||||
background-size: cover;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
</body>
|
||||
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
|
||||
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
|
||||
<script src="@routes.Assets.versioned("javascripts/ingame.js")" type="text/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
</html>
|
||||
|
||||
@@ -27,4 +27,7 @@
|
||||
<div class="btn btn-success" onclick="createGameJS()">Create Game</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
<script>
|
||||
disconnectWebSocket();
|
||||
</script>
|
||||
@@ -175,3 +175,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
disconnectWebSocket();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user