package scalevalapokalypsi.Server // TODO: TLS/SSL / import javax.net.ssl.SSLServerSocketFactory import scalevalapokalypsi.Model.{Adventure, Event} import scalevalapokalypsi.Model.Entities.Player import scalevalapokalypsi.constants.* import scalevalapokalypsi.utils.stringToByteArray import java.io.IOException import java.lang.System.currentTimeMillis import java.lang.Thread.sleep import java.net.{ServerSocket, Socket} /** `Server` exists to initialize a server for the game * and run it with its method `startServer`. * * @param port the TCP port the server should listen on * @param maxClients the maximum number of clients that may be in the game * simultaneously. * @param timeLimit the time limit clients should have to execute their turns. * @param joinAfterStart whether new clients are accepted after the game has * been started */ class Server( port: Int, maxClients: Int, val timeLimit: Int, val joinAfterStart: Boolean ): private val socket = ServerSocket(port) private val clientGetter = ConnectionGetter(socket) private val clients: Clients = Clients(maxClients) private val buffer: Array[Byte] = Array.ofDim(1024) private var bufferIndex = 0 private var adventure: Option[Adventure] = None private var previousTurn = 0.0 /** Starts the server. Won't terminate under normal circumstances. */ def startServer(): Unit = while true do this.serverStep() sleep(POLL_INTERVAL) private def serverStep(): Unit = this.clients.removeNonCompliant() if this.adventure.isEmpty || this.joinAfterStart then this.receiveNewClient() this.readFromAll() this.clients.foreach(_.interpretData()) this.makeClientsSing() this.writeClientDataToClients() this.writeObservations() this.endGameForDeadClients() if adventure.exists(_.isWon) then this.winGame() if this.canExecuteTurns then this.clients.foreach(_.giveTurn()) this.adventure.foreach(_.takeNpcTurns()) this.clients.foreach(c => this.writeToClient(this.turnStartInfo(c), c) ) this.previousTurn = currentTimeMillis().toDouble / 1000.0 if this.adventure.isDefined && this.joinAfterStart then this.clients.foreach( c => if c.isReadyForGameStart then this.adventure.foreach(a => c.getName.foreach(n => a.addPlayer(n)) ) startGameForClient(c) ) else if this.adventure.isEmpty && !this.clients.isEmpty && this.clients.forall(_.isReadyForGameStart) then this.adventure = Some(Adventure(this.clients.names)) this.clients.foreach(startGameForClient(_)) this.previousTurn = currentTimeMillis().toDouble / 1000.0 /** Helper function to start the game for the specified client c. * MAY ONLY BE USED IF `this.adventure` is Some! * Apparently guard clauses are bad because they use return or something, * but assertions should be fine, as long as they enforce the function * contract? */ private def startGameForClient(c: Client): Unit = assert(this.adventure.isDefined) c.gameStart() val name = c.getName val playerEntity: Option[Player] = name match case Some(n) => this.adventure match case Some(a) => a.getPlayer(n) case None => None case None => None playerEntity.foreach(c.givePlayer(_)) this.writeToClient( s"$timeLimit\r\n${this.turnStartInfo(c)}", c ) val joinEvent = c.player.map(p => Event( Map.from(Vector((p, ""))), s"${p.name} liittyy peliin." )) joinEvent.foreach(ev => this.clients.foreach(cl => if cl != c then cl.player.foreach(_.observe(ev)) )) private def writeObservations(): Unit = this.clients.foreach(c => val observations = c.player.map(_.readAndClearObservations()) observations.foreach(_.foreach((s: String) => this.writeToClient(s"$ACTION_NONBLOCKING_INDICATOR$s\r\n", c)) ) ) private def makeClientsSing(): Unit = this.clients.foreach(c => c.startSongIfNeeded() ) /** Helper function to determine if the next turn can be taken */ private def canExecuteTurns: Boolean = val requirement1 = this.adventure.isDefined val requirement2 = !this.clients.isEmpty // nice! you can just return // to the game after everyone // left and everything is just // as before! val allPlayersReady = this.clients.forall(_.hasActed) val requirement3 = (allPlayersReady || currentTimeMillis() / 1000 >= previousTurn + timeLimit) requirement1 && requirement2 && requirement3 /** Receives a new client and stores it in `clients`. * * @return describes if a client was added */ private def receiveNewClient(): Boolean = this.clientGetter.newClient() match case Some(c) => clients.addClient(Client(c)) true case None => false private def turnStartInfo(client: Client): String = val clientArea = client.player.map(_.location) val areaDesc = clientArea .map(_.description) .getOrElse("You are floating in the middle of a soothing void.") val directions = clientArea .map(_.getNeighborNames.mkString(LIST_SEPARATOR)) .getOrElse("") val items = clientArea .map(_.getItemNames.mkString(LIST_SEPARATOR)) .getOrElse("") val entities = client.player.map(c => c.location .getEntityNames .filter(c.name != _) .mkString(LIST_SEPARATOR) ).getOrElse("") s"$TURN_INDICATOR\r\n$areaDesc\r\n$directions\r\n$items\r\n$entities\r\n" /** Sends `message` to all clients * * @param message the message to send */ private def writeToAll(message: String): Unit = this.clients.mapAndRemove(c => val output = c.socket.getOutputStream output.write(stringToByteArray(message)) output.flush() ) private def endGameForDeadClients(): Unit = this.clients.removeNonSatisfying(c => c.player.forall((p: Player) => if !p.isAlive then this.writeToClient(s"$GAME_END_INDICATOR\r\n", c) false else true ) ) private def winGame(): Unit = this.clients.foreach(c => val winEvent = c.player.map(p => Event( Map.empty, s"Onneksi olkoon, voititte pelin!" )) winEvent.foreach(ev => this.clients.foreach(cl => if cl != c then cl.player.foreach(_.observe(ev)) )) ) sleep(1000) this.clients.foreach(c => c.player.foreach((p: Player) => this.writeToClient(s"$GAME_END_INDICATOR\r\n", c) false ) ) private def writeToClient(message: String, client: Client): Unit = try { val output = client.socket.getOutputStream output.write(stringToByteArray(message)) output.flush() } catch { case e: IOException => client.failedProtocol() } /** Sends every client's `dataToThisClient` to the client */ private def writeClientDataToClients(): Unit = this.clients.mapAndRemove(c => val output = c.socket.getOutputStream val data = c.dataToThisClient() output.write(stringToByteArray(data)) output.flush() ) /** Reads data sent by clients and stores it in the `Client`s of `clients` */ private def readFromAll(): Unit = clients.mapAndRemove(c => val input = c.socket.getInputStream while input.available() != 0 do val bytesRead = input.read(buffer) if bytesRead != -1 then c.receiveData(buffer.take(bytesRead).toVector) ) end Server