feat(websocket)!: Implement WebSocket connection and event handling
This commit is contained in:
17
build.sbt
17
build.sbt
@@ -1,12 +1,12 @@
|
||||
ThisBuild / scalaVersion := "3.5.1"
|
||||
|
||||
lazy val commonSettings = Seq(
|
||||
libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.18",
|
||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.18" % "test",
|
||||
libraryDependencies += "io.github.mkpaz" % "atlantafx-base" % "2.0.1",
|
||||
libraryDependencies += "org.scalafx" %% "scalafx" % "22.0.0-R33",
|
||||
libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "2.3.0",
|
||||
libraryDependencies += "org.playframework" %% "play-json" % "3.1.0-M1",
|
||||
libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.19",
|
||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % "test",
|
||||
libraryDependencies += "io.github.mkpaz" % "atlantafx-base" % "2.1.0",
|
||||
libraryDependencies += "org.scalafx" %% "scalafx" % "24.0.2-R36",
|
||||
libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "2.4.0",
|
||||
libraryDependencies += "org.playframework" %% "play-json" % "3.1.0-M9",
|
||||
libraryDependencies ++= {
|
||||
// Determine OS version of JavaFX binaries
|
||||
lazy val osName = System.getProperty("os.name") match {
|
||||
@@ -38,8 +38,9 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
|
||||
commonSettings,
|
||||
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test,
|
||||
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12",
|
||||
libraryDependencies += "com.auth0" % "java-jwt" % "4.3.0",
|
||||
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.2",
|
||||
libraryDependencies += "com.auth0" % "java-jwt" % "4.5.0",
|
||||
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.3",
|
||||
libraryDependencies += "tools.jackson.module" %% "jackson-module-scala" % "3.0.2",
|
||||
JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
|
||||
)
|
||||
|
||||
|
||||
Submodule knockoutwhist updated: e4322839d1...ec94ecd46c
@@ -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>
|
||||
|
||||
@@ -41,4 +41,4 @@ POST /game/:id/returnToLobby controllers.IngameController.returnT
|
||||
GET /polling/:gameId controllers.PollingController.polling(gameId: String)
|
||||
|
||||
# Websocket
|
||||
GET /websocket controllers.WebsocketController.socket()
|
||||
GET /websocket controllers.WebsocketController.socket()
|
||||
187
knockoutwhistweb/public/javascripts/ingame.js
Normal file
187
knockoutwhistweb/public/javascripts/ingame.js
Normal file
@@ -0,0 +1,187 @@
|
||||
// javascript
|
||||
let ws = null; // will be created by connectWebSocket()
|
||||
const pending = new Map(); // id -> { resolve, reject, timer }
|
||||
const handlers = new Map(); // eventType -> handler(data) -> (value|Promise)
|
||||
|
||||
|
||||
let timer = null;
|
||||
// helper to attach message/error/close handlers to a socket
|
||||
function setupSocketHandlers(socket) {
|
||||
socket.onmessage = (event) => {
|
||||
console.debug("SERVER RESPONSE:", event.data);
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(event.data);
|
||||
} catch (e) {
|
||||
console.debug("Non-JSON message from server:", event.data, e);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = msg.id;
|
||||
const eventType = msg.event;
|
||||
const status = msg.status;
|
||||
const data = msg.data;
|
||||
|
||||
if (id && typeof status === "string") {
|
||||
const entry = pending.get(id);
|
||||
if (!entry) return;
|
||||
clearTimeout(entry.timer);
|
||||
pending.delete(id);
|
||||
|
||||
if (status === "success") {
|
||||
entry.resolve(data === undefined ? {} : data);
|
||||
} else {
|
||||
entry.reject(new Error(msg.error || "Server returned error"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (id && eventType) {
|
||||
const handler = handlers.get(eventType);
|
||||
const sendResponse = (respData) => {
|
||||
const response = { id: id, event: eventType, data: respData === undefined ? {} : respData };
|
||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify(response));
|
||||
} else {
|
||||
console.warn("Cannot send response, websocket not open");
|
||||
}
|
||||
};
|
||||
|
||||
if (!handler) {
|
||||
// no handler: respond with an error object in data so server can fail it
|
||||
sendResponse({ error: "No handler for event: " + eventType });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Promise.resolve(handler(data === undefined ? {} : data))
|
||||
.then(result => sendResponse(result))
|
||||
.catch(err => sendResponse({ error: err?.message ? err.message : String(err) }));
|
||||
} catch (err) {
|
||||
sendResponse({ error: err?.message ? err.message : String(err) });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = (error) => {
|
||||
console.error("WebSocket Error:", error);
|
||||
if (timer) clearInterval(timer);
|
||||
for (const [id, entry] of pending.entries()) {
|
||||
clearTimeout(entry.timer);
|
||||
entry.reject(new Error("WebSocket error/closed"));
|
||||
pending.delete(id);
|
||||
}
|
||||
if (socket.readyState === WebSocket.OPEN) socket.close(1000, "Unexpected error.");
|
||||
};
|
||||
|
||||
socket.onclose = (event) => {
|
||||
if (timer) clearInterval(timer);
|
||||
for (const [id, entry] of pending.entries()) {
|
||||
clearTimeout(entry.timer);
|
||||
entry.reject(new Error("WebSocket closed"));
|
||||
pending.delete(id);
|
||||
}
|
||||
if (event.wasClean) {
|
||||
console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`);
|
||||
} else {
|
||||
console.warn('Connection died unexpectedly.');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// connect/disconnect helpers
|
||||
function connectWebSocket(url = "ws://localhost:9000/websocket") {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) return Promise.resolve();
|
||||
if (ws && ws.readyState === WebSocket.CONNECTING) {
|
||||
// already connecting - return a promise that resolves on open
|
||||
return new Promise((resolve, reject) => {
|
||||
const prevOnOpen = ws.onopen;
|
||||
const prevOnError = ws.onerror;
|
||||
ws.onopen = (ev) => {
|
||||
if (prevOnOpen) prevOnOpen(ev);
|
||||
resolve();
|
||||
};
|
||||
ws.onerror = (err) => {
|
||||
if (prevOnError) prevOnError(err);
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
ws = new WebSocket(url);
|
||||
setupSocketHandlers(ws);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connection established!");
|
||||
// start heartbeat
|
||||
timer = setInterval(() => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
sendEventAndWait("ping", {}).then(
|
||||
() => console.debug("PING RESPONSE RECEIVED"),
|
||||
).catch(
|
||||
(err) => console.warn("PING ERROR:", err.message),
|
||||
);
|
||||
console.debug("PING SENT");
|
||||
}
|
||||
}, 5000);
|
||||
resolve();
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function disconnectWebSocket(code = 1000, reason = "Client disconnect") {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
if (ws) {
|
||||
try { ws.close(code, reason); } catch (e) {}
|
||||
ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
function sendEvent(eventType, eventData) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
console.warn("WebSocket is not open. Unable to send message.");
|
||||
return;
|
||||
}
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
|
||||
const message = { id: id, event: eventType, data: eventData };
|
||||
ws.send(JSON.stringify(message));
|
||||
console.debug("SENT:", message);
|
||||
}
|
||||
|
||||
function sendEventAndWait(eventType, eventData, timeoutMs = 10000) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
return Promise.reject(new Error("WebSocket is not open"));
|
||||
}
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 9);
|
||||
const message = { id: id, event: eventType, data: eventData };
|
||||
const p = new Promise((resolve, reject) => {
|
||||
const timerId = setTimeout(() => {
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id);
|
||||
reject(new Error(`No response within ${timeoutMs}ms for id=${id}`));
|
||||
}
|
||||
}, timeoutMs);
|
||||
pending.set(id, { resolve, reject, timer: timerId });
|
||||
});
|
||||
ws.send(JSON.stringify(message));
|
||||
console.debug("SENT (await):", message);
|
||||
return p;
|
||||
}
|
||||
function onEvent(eventType, handler) {
|
||||
handlers.set(eventType, handler);
|
||||
}
|
||||
|
||||
globalThis.sendEvent = sendEvent;
|
||||
globalThis.sendEventAndWait = sendEventAndWait;
|
||||
globalThis.onEvent = onEvent;
|
||||
globalThis.connectWebSocket = connectWebSocket;
|
||||
globalThis.disconnectWebSocket = disconnectWebSocket;
|
||||
globalThis.isWebSocketConnected = () => !!ws && ws.readyState === WebSocket.OPEN;
|
||||
@@ -664,23 +664,4 @@ function sendPlayCardRequest(jsonObj, gameId, cardobject, dog) {
|
||||
})
|
||||
})
|
||||
}
|
||||
const ws = new WebSocket("ws://localhost:9000/websocket");
|
||||
ws.onopen = (event) => {
|
||||
console.log("WebSocket connection established!");
|
||||
|
||||
ws.send("Client is now connected and ready.");
|
||||
};
|
||||
ws.onmessage = (event) => {
|
||||
console.log("SERVER RESPONSE:", event.data);
|
||||
};
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket Error:", error);
|
||||
};
|
||||
ws.onclose = (event) => {
|
||||
if (event.wasClean) {
|
||||
console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`);
|
||||
} else {
|
||||
console.warn('Connection died unexpectedly.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user