feat(ui): Websocket
Started implementing functionality to Websocket.
This commit is contained in:
@@ -40,8 +40,8 @@ lazy val knockoutwhistweb = project.in(file("knockoutwhistweb"))
|
|||||||
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12",
|
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12",
|
||||||
libraryDependencies += "com.auth0" % "java-jwt" % "4.5.0",
|
libraryDependencies += "com.auth0" % "java-jwt" % "4.5.0",
|
||||||
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.3",
|
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.2.3",
|
||||||
libraryDependencies += "tools.jackson.module" %% "jackson-module-scala" % "3.0.2",
|
libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.16.1",
|
||||||
JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
|
//JsEngineKeys.engineType := JsEngineKeys.EngineType.Node
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy val root = (project in file("."))
|
lazy val root = (project in file("."))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package logic.game
|
|||||||
|
|
||||||
import de.knockoutwhist.cards.{Hand, Suit}
|
import de.knockoutwhist.cards.{Hand, Suit}
|
||||||
import de.knockoutwhist.control.GameLogic
|
import de.knockoutwhist.control.GameLogic
|
||||||
import de.knockoutwhist.control.GameState.{Lobby, MainMenu}
|
import de.knockoutwhist.control.GameState.{InGame, Lobby, MainMenu}
|
||||||
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
|
import de.knockoutwhist.control.controllerBaseImpl.sublogic.util.{MatchUtil, PlayerUtil}
|
||||||
import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed}
|
import de.knockoutwhist.events.global.{GameStateChangeEvent, SessionClosed}
|
||||||
import de.knockoutwhist.events.player.PlayerEvent
|
import de.knockoutwhist.events.player.PlayerEvent
|
||||||
@@ -59,6 +59,9 @@ class GameLobby private(
|
|||||||
if (event.oldState == MainMenu && event.newState == Lobby) {
|
if (event.oldState == MainMenu && event.newState == Lobby) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.oldState == Lobby && event.newState == InGame) {
|
||||||
|
println("RECEIVED GAMESTATEEVENT")
|
||||||
|
}
|
||||||
users.values.foreach(session => session.updatePlayer(event))
|
users.values.foreach(session => session.updatePlayer(event))
|
||||||
case event: SimpleEvent =>
|
case event: SimpleEvent =>
|
||||||
users.values.foreach(session => session.updatePlayer(event))
|
users.values.foreach(session => session.updatePlayer(event))
|
||||||
@@ -71,6 +74,7 @@ class GameLobby private(
|
|||||||
* @param user the user who wants to start the game.
|
* @param user the user who wants to start the game.
|
||||||
*/
|
*/
|
||||||
def startGame(user: User): Unit = {
|
def startGame(user: User): Unit = {
|
||||||
|
println("STARTED GAME IN LOGIC")
|
||||||
val sessionOpt = users.get(user.id)
|
val sessionOpt = users.get(user.id)
|
||||||
if (sessionOpt.isEmpty) {
|
if (sessionOpt.isEmpty) {
|
||||||
throw new NotInThisGameException("You are not in this game!")
|
throw new NotInThisGameException("You are not in this game!")
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package model.sessions
|
|||||||
|
|
||||||
import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
|
import de.knockoutwhist.events.player.{RequestCardEvent, RequestTieChoiceEvent, RequestTrumpSuitEvent}
|
||||||
import de.knockoutwhist.utils.events.SimpleEvent
|
import de.knockoutwhist.utils.events.SimpleEvent
|
||||||
|
import logic.PodManager
|
||||||
import logic.game.GameLobby
|
import logic.game.GameLobby
|
||||||
import model.users.User
|
import model.users.User
|
||||||
import play.api.libs.json.JsObject
|
import play.api.libs.json.Format.GenericFormat
|
||||||
|
import play.api.libs.json.{JsError, JsObject, JsResult, JsSuccess, JsValue}
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
@@ -26,7 +28,7 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
|
|||||||
else canInteract = Some(InteractionType.Card)
|
else canInteract = Some(InteractionType.Card)
|
||||||
case _ =>
|
case _ =>
|
||||||
}
|
}
|
||||||
websocketActor.foreach(_.transmitEventToClient(event))
|
websocketActor.foreach(_.transmitEventToClient(event, gameLobby))
|
||||||
}
|
}
|
||||||
|
|
||||||
override def id: UUID = user.id
|
override def id: UUID = user.id
|
||||||
@@ -44,6 +46,21 @@ class UserSession(val user: User, val host: Boolean, val gameLobby: GameLobby) e
|
|||||||
case "Ping" =>
|
case "Ping" =>
|
||||||
// No action needed for Ping
|
// No action needed for Ping
|
||||||
()
|
()
|
||||||
|
case "Start Game" =>
|
||||||
|
println("INSIDE HANDLE WEB RESPONSE" + data)
|
||||||
|
val gameId: String = (data \ "gameId").get.toString
|
||||||
|
val cleanGameId: String = gameId.replaceAll("^[\"']|[\"']$", "")
|
||||||
|
val user: JsObject = (data \ "user").asOpt[JsObject].get
|
||||||
|
val gameLobby: GameLobby = PodManager.getGame(cleanGameId).get
|
||||||
|
val realUser: JsResult[User] = user.validate[User]
|
||||||
|
val uu: User = realUser match {
|
||||||
|
case JsSuccess(extractedUser, _) =>
|
||||||
|
extractedUser
|
||||||
|
case e: JsError =>
|
||||||
|
println("FAILED" + JsError.toJson(e).toString())
|
||||||
|
throw new Exception("Failed to deserialize User object: " + JsError.toJson(e).toString())
|
||||||
|
}
|
||||||
|
gameLobby.startGame(uu)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lock.unlock()
|
lock.unlock()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package model.users
|
package model.users
|
||||||
|
|
||||||
|
import play.api.libs.json.{Format, Json}
|
||||||
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
case class User(
|
case class User(
|
||||||
@@ -16,5 +18,7 @@ case class User(
|
|||||||
private def withPasswordHash(newPasswordHash: String): User = {
|
private def withPasswordHash(newPasswordHash: String): User = {
|
||||||
this.copy(passwordHash = newPasswordHash)
|
this.copy(passwordHash = newPasswordHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
object User {
|
||||||
|
implicit val userFormat: Format[User] = Json.format[User]
|
||||||
|
}
|
||||||
@@ -27,6 +27,10 @@ object WebsocketEventMapper {
|
|||||||
registerCustomMapper(ReceivedHandEventMapper)
|
registerCustomMapper(ReceivedHandEventMapper)
|
||||||
registerCustomMapper(GameStateEventMapper)
|
registerCustomMapper(GameStateEventMapper)
|
||||||
registerCustomMapper(CardPlayedEventMapper)
|
registerCustomMapper(CardPlayedEventMapper)
|
||||||
|
registerCustomMapper(NewRoundEventMapper)
|
||||||
|
registerCustomMapper(NewTrickEventMapper)
|
||||||
|
registerCustomMapper(TrickEndEventMapper)
|
||||||
|
registerCustomMapper(RequestCardEventMapper)
|
||||||
registerCustomMapper(LobbyUpdateEventMapper)
|
registerCustomMapper(LobbyUpdateEventMapper)
|
||||||
registerCustomMapper(LeftEventMapper)
|
registerCustomMapper(LeftEventMapper)
|
||||||
registerCustomMapper(KickEventMapper)
|
registerCustomMapper(KickEventMapper)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.global.GameStateChangeEvent
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object GameStateChangeEventMapper extends SimpleEventMapper[GameStateChangeEvent]{
|
||||||
|
override def id: String = "GameStateChangeEvent"
|
||||||
|
|
||||||
|
override def toJson(event: GameStateChangeEvent, gameLobby: GameLobby): JsObject = {
|
||||||
|
println("CALLED toJSON FOR GAMESTATECHANGE")
|
||||||
|
Json.obj(
|
||||||
|
"oldState" -> event.oldState.toString,
|
||||||
|
"newState" -> event.newState.toString,
|
||||||
|
"gameLobby" -> gameLobby.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
knockoutwhistweb/app/util/mapper/NewRoundEventMapper.scala
Normal file
16
knockoutwhistweb/app/util/mapper/NewRoundEventMapper.scala
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.global.NewRoundEvent
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object NewRoundEventMapper extends SimpleEventMapper[NewRoundEvent]{
|
||||||
|
override def id: String = "NewRoundEvent"
|
||||||
|
|
||||||
|
override def toJson(event: NewRoundEvent, gameLobby: GameLobby): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"trumpsuit" -> gameLobby.getLogic.getCurrentRound.get.trumpSuit.toString,
|
||||||
|
"players" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.toString)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
knockoutwhistweb/app/util/mapper/NewTrickEventMapper.scala
Normal file
13
knockoutwhistweb/app/util/mapper/NewTrickEventMapper.scala
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.global.NewTrickEvent
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object NewTrickEventMapper extends SimpleEventMapper[NewTrickEvent]{
|
||||||
|
override def id: String = "NewTrickEvent"
|
||||||
|
|
||||||
|
override def toJson(event: NewTrickEvent, gameLobby: GameLobby): JsObject = {
|
||||||
|
Json.obj()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.player.RequestCardEvent
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object RequestCardEventMapper extends SimpleEventMapper[RequestCardEvent]{
|
||||||
|
override def id: String = "RequestCardEvent"
|
||||||
|
|
||||||
|
override def toJson(event: RequestCardEvent, gameLobby: GameLobby): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"player" -> event.player.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
knockoutwhistweb/app/util/mapper/TrickEndEventMapper.scala
Normal file
18
knockoutwhistweb/app/util/mapper/TrickEndEventMapper.scala
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package util.mapper
|
||||||
|
|
||||||
|
import de.knockoutwhist.events.global.TrickEndEvent
|
||||||
|
import de.knockoutwhist.rounds.Trick
|
||||||
|
import logic.game.GameLobby
|
||||||
|
import play.api.libs.json.{JsObject, Json}
|
||||||
|
|
||||||
|
object TrickEndEventMapper extends SimpleEventMapper[TrickEndEvent]{
|
||||||
|
override def id: String = "TrickEndEvent"
|
||||||
|
|
||||||
|
override def toJson(event: TrickEndEvent, gameLobby: GameLobby): JsObject = {
|
||||||
|
Json.obj(
|
||||||
|
"playerwon" -> event.winner.name,
|
||||||
|
"playersin" -> gameLobby.getLogic.getCurrentMatch.get.playersIn.map(player => player.name),
|
||||||
|
"tricklist" -> gameLobby.getLogic.getCurrentRound.get.tricklist.map(trick => trick.winner.map(player => player.name).getOrElse("Trick in Progress"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 text-center mb-5">
|
<div class="col-12 text-center mb-5">
|
||||||
<div class="btn btn-success" onclick="startGame('@gamelobby.id')">Start Game</div>
|
<div class="btn btn-success" onclick="startGame('@gamelobby.id', '@user.get.id', '@user.get.name','@user.get.passwordHash', @user.get.internalId)">Start Game</div>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<div id="players" class="justify-content-center align-items-center d-flex">
|
<div id="players" class="justify-content-center align-items-center d-flex">
|
||||||
@@ -98,6 +98,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
|
/*
|
||||||
function waitForFunction(name, checkInterval = 100) {
|
function waitForFunction(name, checkInterval = 100) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
@@ -109,5 +110,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
|
waitForFunction("pollForUpdates").then(fn => fn('@gamelobby.id'));
|
||||||
|
*/
|
||||||
connectWebSocket()
|
connectWebSocket()
|
||||||
</script>
|
</script>
|
||||||
@@ -30,4 +30,4 @@
|
|||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
disconnectWebSocket();
|
disconnectWebSocket();
|
||||||
</script>
|
</script>
|
||||||
@@ -1,3 +1,30 @@
|
|||||||
|
function alertMessage(message) {
|
||||||
|
let newHtml = '';
|
||||||
|
const alertId = `alert-${Date.now()}`;
|
||||||
|
const fadeTime = 500;
|
||||||
|
const duration = 5000;
|
||||||
|
newHtml += `
|
||||||
|
<div class="fixed-top d-flex justify-content-center mt-3" style="z-index: 1050;">
|
||||||
|
<div
|
||||||
|
id="${alertId}" class="alert alert-primary d-flex align-items-center p-2 mb-0 w-auto" role="alert" style="display: none;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="bi flex-shrink-0 me-2" viewBox="0 0 16 16" role="img" aria-label="Warning:">
|
||||||
|
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||||
|
</svg>
|
||||||
|
<div class="small">
|
||||||
|
<small>${message}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
$('#main-body').prepend(newHtml);
|
||||||
|
const $notice = $(`#${alertId}`);
|
||||||
|
$notice.fadeIn(fadeTime);
|
||||||
|
setTimeout(function() {
|
||||||
|
$notice.fadeOut(fadeTime, function() {
|
||||||
|
$(this).parent().remove();
|
||||||
|
});
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
function receiveHandEvent(eventData) {
|
function receiveHandEvent(eventData) {
|
||||||
//Data
|
//Data
|
||||||
const dog = eventData.dog;
|
const dog = eventData.dog;
|
||||||
@@ -38,6 +65,101 @@ function receiveHandEvent(eventData) {
|
|||||||
}
|
}
|
||||||
handElement.html(newHtml);
|
handElement.html(newHtml);
|
||||||
}
|
}
|
||||||
|
function newRoundEvent(eventData) {
|
||||||
|
const trumpsuit = eventData.trumpsuit;
|
||||||
|
const players = eventData.players;
|
||||||
|
|
||||||
|
const tableElement = $('#score-table-body');
|
||||||
|
|
||||||
|
|
||||||
|
let tablehtml = `
|
||||||
|
<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between score-header pb-1">
|
||||||
|
<div style="width: 50%">PLAYER</div>
|
||||||
|
<div style="width: 50%">TRICKS</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
players.forEach(
|
||||||
|
tablehtml += `
|
||||||
|
<div class="d-flex justify-content-between score-row pt-1">
|
||||||
|
<div style="width: 50%" class="text-truncate">'${players}'</div>
|
||||||
|
<div style="width: 50%">
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
tableElement.html(tablehtml);
|
||||||
|
|
||||||
|
const trumpsuitClass = $('#trumpsuit');
|
||||||
|
trumpsuitClass.html(trumpsuit);
|
||||||
|
|
||||||
|
}
|
||||||
|
function trickEndEvent(eventData) {
|
||||||
|
const winner = eventData.playerwon;
|
||||||
|
const players = eventData.playersin;
|
||||||
|
const tricklist = eventData.tricklist;
|
||||||
|
|
||||||
|
let newHtml = '';
|
||||||
|
|
||||||
|
let tricktable = $('#score-table-body');
|
||||||
|
|
||||||
|
newHtml += `
|
||||||
|
<h4 class="fw-bold mb-3 text-black">Tricks Won</h4>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between score-header pb-1">
|
||||||
|
<div style="width: 50%">PLAYER</div>
|
||||||
|
<div style="width: 50%">TRICKS</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
let playercounts = new Map();
|
||||||
|
|
||||||
|
players.forEach( player => {
|
||||||
|
playercounts.set(player, 0)
|
||||||
|
});
|
||||||
|
tricklist.forEach( player => {
|
||||||
|
if ( player !== "Trick in Progress" && playercounts.has(player)) {
|
||||||
|
playercounts.set(player, playercounts.get(player) + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const playerorder = players.sort((playerA, playerB) => {
|
||||||
|
const countA = playercounts.get(playerA.name) || 0;
|
||||||
|
const countB = playercounts.get(playerB.name) || 0;
|
||||||
|
return countB - countA;
|
||||||
|
});
|
||||||
|
playerorder.forEach( player => {
|
||||||
|
newHtml += `
|
||||||
|
<div class="d-flex justify-content-between score-row pt-1">
|
||||||
|
<div style="width: 50%" class="text-truncate">'${player}'</div>
|
||||||
|
<div style="width: 50%">
|
||||||
|
'${playercounts.get(player)}'
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
tricktable.html(newHtml);
|
||||||
|
}
|
||||||
|
function newTrickEvent() {
|
||||||
|
const firstCardContainer = $('first-card-container');
|
||||||
|
|
||||||
|
let newHtml = '';
|
||||||
|
|
||||||
|
newHtml += `
|
||||||
|
<img src="images/cards/1B.png" alt="Blank Card" width="80px"/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
firstCardContainer.html(newHtml);
|
||||||
|
}
|
||||||
|
function requestCardEvent(eventData) {
|
||||||
|
const player = eventData.player;
|
||||||
|
const handElement = $('#card-slide')
|
||||||
|
handElement.removeClass('inactive');
|
||||||
|
}
|
||||||
|
//alertMessage("It worked!")
|
||||||
|
|
||||||
|
|
||||||
function receiveGameStateChange(eventData) {
|
function receiveGameStateChange(eventData) {
|
||||||
const content = eventData.content;
|
const content = eventData.content;
|
||||||
@@ -218,6 +340,10 @@ function receiveTurnEvent(eventData) {
|
|||||||
|
|
||||||
onEvent("ReceivedHandEvent", receiveHandEvent)
|
onEvent("ReceivedHandEvent", receiveHandEvent)
|
||||||
onEvent("GameStateChangeEvent", receiveGameStateChange)
|
onEvent("GameStateChangeEvent", receiveGameStateChange)
|
||||||
|
onEvent("NewRoundEvent", newRoundEvent)
|
||||||
|
onEvent("TrickEndEvent", trickEndEvent)
|
||||||
|
onEvent("NewTrickEvent", newTrickEvent)
|
||||||
|
onEvent("RequestCardEvent", requestCardEvent)
|
||||||
onEvent("CardPlayedEvent", receiveCardPlayedEvent)
|
onEvent("CardPlayedEvent", receiveCardPlayedEvent)
|
||||||
onEvent("LobbyUpdateEvent", receiveLobbyUpdateEvent)
|
onEvent("LobbyUpdateEvent", receiveLobbyUpdateEvent)
|
||||||
onEvent("LeftEvent", receiveGameStateChange)
|
onEvent("LeftEvent", receiveGameStateChange)
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ function handlePlayCard(card, dog) {
|
|||||||
function handleSkipDogLife(button) {
|
function handleSkipDogLife(button) {
|
||||||
// TODO needs implementation
|
// TODO needs implementation
|
||||||
}
|
}
|
||||||
|
function startGame(gameId, userId, username, userpasswordhash, userinternalid) {
|
||||||
|
const userpayload = {
|
||||||
|
internalId: userinternalid,
|
||||||
|
id: userId,
|
||||||
|
name: username,
|
||||||
|
passwordHash: userpasswordhash
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
gameId: gameId,
|
||||||
|
user: userpayload
|
||||||
|
};
|
||||||
|
sendEvent("Start Game", payload)
|
||||||
|
}
|
||||||
function handleKickPlayer(playerId) {
|
function handleKickPlayer(playerId) {
|
||||||
// TODO needs implementation
|
// TODO needs implementation
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user