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/main/scala/Server | |
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/main/scala/Server')
-rw-r--r-- | src/main/scala/Server/Client.scala | 173 | ||||
-rw-r--r-- | src/main/scala/Server/Clients.scala | 82 | ||||
-rw-r--r-- | src/main/scala/Server/ConnectionGetter.scala | 25 | ||||
-rw-r--r-- | src/main/scala/Server/Server.scala | 195 | ||||
-rw-r--r-- | src/main/scala/Server/constants.scala | 19 |
5 files changed, 0 insertions, 494 deletions
diff --git a/src/main/scala/Server/Client.scala b/src/main/scala/Server/Client.scala deleted file mode 100644 index 3cd2b36..0000000 --- a/src/main/scala/Server/Client.scala +++ /dev/null @@ -1,173 +0,0 @@ -package o1game.Server - -import java.net.Socket -import scala.math.min -import o1game.constants.* -import ServerProtocolState.* -import o1game.Model.{Action,Player,Entity} - -class Client(val socket: Socket): - private var incompleteMessage: Array[Byte] = - Array.fill(MAX_MSG_SIZE)(0.toByte) - private var incompleteMessageIndex = 0 - private var protocolState = WaitingForVersion - private var outData: String = "" - private var character: Option[Player] = None - private var protocolIsIntact = true - private var name: Option[String] = None - private var nextAction: Option[Action] = None - - /** Calculates the amount of bytes available for future incoming messages */ - def spaceAvailable: Int = MAX_MSG_SIZE - incompleteMessageIndex - - /** Tests whether the client has behaved according to protocol. - * - * @return false if there has been a protocol violation, true otherwise - */ - def isIntactProtocolWise: Boolean = this.protocolIsIntact - - /** Marks that this client misbehaved in eyes of the protocol */ - def failedProtocol(): Unit = this.protocolIsIntact = false - - /** Tests whether this client is initialized and ready to start the game - * - * @return true if the client is ready to join the game - */ - def isReadyForGameStart: Boolean = - this.protocolState == WaitingForGameStart - - /** Signals this client that it's joining the game. This is important so - * that this object knows to update its protocol state. - */ - def gameStart(): Unit = this.protocolState = InGame - - /** Returns the player this client controls in the model. - * - * @return an option containing the player - */ - def player: Option[Player] = this.character - - /** Tells this client object that it controls the specified player. - * - * @param player the player this client is to control - */ - def givePlayer(player: Player): Unit = - this.character = Some(player) - - /** Gets the name of this client, which should match the name of the player - * that is given to this client. Not very useful if the client hasn't yet - * received the name or if it already has an player. - * - * @return the name of this client - */ - def getName: Option[String] = this.name - - /** Sets `data` as received for the client. - * - * @return false means there was not enough space to receive the message - */ - def receiveData(data: Vector[Byte]): Boolean = - for i <- 0 until min(data.length, spaceAvailable) do - this.incompleteMessage(this.incompleteMessageIndex + i) = data(i) - this.incompleteMessageIndex += data.length - this.incompleteMessageIndex = - min(this.incompleteMessageIndex, MAX_MSG_SIZE) - data.length < spaceAvailable - - /** Returns data that should be sent to this client. - * The data is cleared when calling. - */ - def dataToThisClient(): String = - val a = this.outData - this.outData = "" - a - - /** Specifies that the data should be buffered for - * sending to this client - * - * @param data data to buffer for sending - */ - private def addDataToSend(data: String): Unit = - this.outData += s"$data\r\n" - - - /** Returns one line of data if there are any line breaks. - * Removes the parsed data from the message buffering area. - */ - private def nextLine(): Option[String] = - var nextCRLF = this.incompleteMessage.indexOf(CRLF(0)) - if this.incompleteMessage(nextCRLF + 1) != CRLF(1) then nextCRLF = -1 - if nextCRLF != -1 then - val message = this.incompleteMessage.take(nextCRLF) - val rest = this.incompleteMessage.drop(nextCRLF + 2) - this.incompleteMessage = rest ++ Array.fill(nextCRLF + 1)(0.toByte) - // TODO: the conversion may probably be exploited to crash the server - Some(String(message)) - else - None - - /** Makes the client play its turn */ - def act(): Unit = - this.addDataToSend(ACTION_BLOCKING_INDICATOR.toString) - this.nextAction.foreach(a => this.addDataToSend( - s"$ACTION_BLOCKING_INDICATOR${this.executeAction(a)}" - )) - this.nextAction = None - - /** Checks whether the client has chosen its next action - * - * @return whether the client is ready to act */ - def isReadyToAct: Boolean = this.nextAction.isDefined - - /** Causes the client to interpret the data it has received */ - def interpretData(): Unit = - LazyList.continually(this.nextLine()) - .takeWhile(_.isDefined) - .flatten - .foreach(s => interpretLine(s)) - - /** Makes the client execute the action specified by `line`. - * If there is a protocol error, the function changes - * the variable `protocolIsIntact` to false. - * - * @param line the line to interpret - */ - private def interpretLine(line: String): Unit = - this.protocolIsIntact = this.protocolState match - case WaitingForVersion => - if line == GAME_VERSION then - addDataToSend(s"$PROTOCOL_VERSION_GOOD") - this.protocolState = WaitingForClientName - true - else - addDataToSend(s"$PROTOCOL_VERSION_BAD") - false - case WaitingForClientName => - this.name = Some(line) - this.protocolState = WaitingForGameStart - true - case WaitingForGameStart => true - case InGame => - this.bufferAction(Action(line)) - true - - /** Buffers the action for execution or executes it immediately if it - * doesn't take a turn */ - private def bufferAction(action: Action) = - if ( - this.nextAction.isEmpty && - this.player.exists(action.takesATurnFor(_)) - ) then - this.nextAction = Some(action) - else if this.nextAction.isEmpty then - this.addDataToSend(s"$ACTION_NONBLOCKING_INDICATOR${this.executeAction(action)}") - - /** Executes the specified action and returns its description */ - private def executeAction(action: Action): String = - this.character.flatMap(action.execute(_)) match - case Some(s) => s - case None => "You can't do that" - - -end Client - diff --git a/src/main/scala/Server/Clients.scala b/src/main/scala/Server/Clients.scala deleted file mode 100644 index 6487446..0000000 --- a/src/main/scala/Server/Clients.scala +++ /dev/null @@ -1,82 +0,0 @@ -package o1game.Server - -import scala.util.Try -import scala.util.Random - -class Clients(maxClients: Int): - private val clients: Array[Option[Client]] = Array.fill(maxClients)(None) - - /** Adds `client` to this collection of clients. - * - * @param client the Client to add - * @return true if there was room for the client - * i.e. fewer clients than `maxClients`, false otherwise - */ - def addClient(client: Client): Boolean = - val i = this.clients.indexOf(None) - if i == -1 then - false - else - this.clients(i) = Some(client) - true - - /** Returns all the clients. - * - * @return an iterable of all the clients - */ - def allClients: Iterable[Client] = clients.toVector.flatten - - /** Applies the function `f` to all the clients for its side effects. */ - def foreach(f: Client => Any): Unit = this.clients.flatten.foreach(f) - - /** Executes the function `f` for all clients in a pseudorandom order. */ - def inRandomOrder(f: Client => Any): Unit = - Random.shuffle(this - .clients - .flatten) - .foreach(f) - - /** Returns true if the predicate `f` stands for all clients, - * false otherwise - * - * @param f the predicate to check for all clients - * @return whether `f` stands for all clients - */ - def forall(f: Client => Boolean): Boolean = this.clients.flatten.forall(f) - - /** Gets the names of all the clients stored by this object. - * - * @return the names of the clients - */ - def names: Vector[String] = this.clients.flatten.flatMap(_.getName).toVector - - def isEmpty: Boolean = this.clients.flatten.isEmpty - - /** Applies the function `f` to all the clients for its side effects - * and removes all the clients for which `f([client])` returns false. - * This is useful for doing IO with the client and removing clients - * with stale sockets. - * - * @param f the function to apply to all the clients and filter them with - */ - def removeNonSatisfying(f: Client => Boolean): Unit = - for i <- this.clients.indices do - this.clients(i) match - case Some(c) => - if !f(c) then - this.clients(i) = None - case None => - - /** Removes clients that have not behaved according to protocol */ - def removeNonCompliant(): Unit = - this.removeNonSatisfying(_.isIntactProtocolWise) - - /** Applies the function f to all clients for its side effects. - * If the function throws an exception, the client is removed. - * Probably a more concise alternative to `removeNonSatisfying`, - * but might catch exceptions unintentionally. - * - * @param f the function to apply for its side effects to each client - */ - def mapAndRemove(f: Client => Unit): Unit = - this.removeNonSatisfying(c => Try(f(c)).isSuccess) diff --git a/src/main/scala/Server/ConnectionGetter.scala b/src/main/scala/Server/ConnectionGetter.scala deleted file mode 100644 index b3246a7..0000000 --- a/src/main/scala/Server/ConnectionGetter.scala +++ /dev/null @@ -1,25 +0,0 @@ -package o1game.Server - -import java.io.IOException -import java.net.{ServerSocket, Socket} -import scala.concurrent.Future -import scala.util.{Failure, Success} -import scala.concurrent.ExecutionContext.Implicits.global - -/** Small helper class for getting new connections using futures */ -class ConnectionGetter(val socket: ServerSocket): - - private var nextClient: Future[Socket] = Future.failed(IOException()) - - /** Returns a new socket to a client if there is any new connections. */ - def newClient(): Option[Socket] = - this.nextClient.value match - case Some(Success(s)) => - nextClient = Future(socket.accept()) - Some(s) - case Some(Failure(e)) => - nextClient = Future(socket.accept()) - None - case None => None - -end ConnectionGetter diff --git a/src/main/scala/Server/Server.scala b/src/main/scala/Server/Server.scala deleted file mode 100644 index 7864c49..0000000 --- a/src/main/scala/Server/Server.scala +++ /dev/null @@ -1,195 +0,0 @@ -package o1game.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 o1game.constants.* -import o1game.Model.{Adventure,Entity,Player} -import o1game.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 diff --git a/src/main/scala/Server/constants.scala b/src/main/scala/Server/constants.scala deleted file mode 100644 index 083db4e..0000000 --- a/src/main/scala/Server/constants.scala +++ /dev/null @@ -1,19 +0,0 @@ - -package o1game.constants - -val MAX_MSG_SIZE = 1024 // bytes -val CRLF: Vector[Byte] = Vector(13.toByte, 10.toByte) -val POLL_INTERVAL = 100 // millisec. -val GAME_VERSION = "0.1.0" -val TURN_INDICATOR = ">" -val ACTION_BLOCKING_INDICATOR='.' -val ACTION_NONBLOCKING_INDICATOR='+' - -val LIST_SEPARATOR=";" - -val PROTOCOL_VERSION_GOOD = "1" -val PROTOCOL_VERSION_BAD = "0" -//assert(PROTOCOL_VERSION_BAD.length <= PROTOCOL_VERSION_GOOD.length) - -enum ServerProtocolState: - case WaitingForVersion, WaitingForClientName, WaitingForGameStart, InGame |