diff options
author | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-17 13:45:44 +0200 |
---|---|---|
committer | Joel Kronqvist <joel.kronqvist@iki.fi> | 2024-11-17 13:45:44 +0200 |
commit | 4de67b497e0e229fe4a42f66f833640b6e50fd5a (patch) | |
tree | 34fb5b0e776f7cd3adcb4556f4d6a7c8ad66de39 /src/scalevalapokalypsi/Server/Server.scala | |
parent | 8595e892abc0e0554f589ed2eb88c351a347fbd4 (diff) | |
download | scalevalapokalypsi-4de67b497e0e229fe4a42f66f833640b6e50fd5a.tar.gz scalevalapokalypsi-4de67b497e0e229fe4a42f66f833640b6e50fd5a.zip |
Moved the project to an IDEA project & wrote part of README.txt
Diffstat (limited to 'src/scalevalapokalypsi/Server/Server.scala')
-rw-r--r-- | src/scalevalapokalypsi/Server/Server.scala | 195 |
1 files changed, 195 insertions, 0 deletions
diff --git a/src/scalevalapokalypsi/Server/Server.scala b/src/scalevalapokalypsi/Server/Server.scala new file mode 100644 index 0000000..13ca2f5 --- /dev/null +++ b/src/scalevalapokalypsi/Server/Server.scala @@ -0,0 +1,195 @@ +package scalevalapokalypsi.Server + + +// TODO: TLS/SSL / import javax.net.ssl.SSLServerSocketFactory + +import java.lang.Thread.{currentThread, sleep} +import java.io.IOException +import java.net.{ServerSocket, Socket} +import scalevalapokalypsi.constants.* +import scalevalapokalypsi.Model.{Adventure,Entity,Player} +import scalevalapokalypsi.utils.stringToByteArray + +import java.lang.System.currentTimeMillis +import scala.util.Try + + +/** `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.writeClientDataToClients() + this.writeObservations() + if this.canExecuteTurns then + this.clients.inRandomOrder(_.act()) + this.writeClientDataToClients() + this.writeObservations() + this.clients.foreach(c => + this.writeToClient(this.turnStartInfo(c), c) + ) + this.previousTurn = currentTimeMillis() / 1000 + 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() / 1000 + + /** 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 + ) + + this.clients.foreach(c => + if c.player != playerEntity then + c.player.foreach(_.observe(s"${name.getOrElse("Unknown player")} joins the game.")) + ) + + + 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)) + ) + ) + + /** 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(_.isReadyToAct) + 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 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 |