aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/Server
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-17 13:45:44 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-17 13:45:44 +0200
commit4de67b497e0e229fe4a42f66f833640b6e50fd5a (patch)
tree34fb5b0e776f7cd3adcb4556f4d6a7c8ad66de39 /src/main/scala/Server
parent8595e892abc0e0554f589ed2eb88c351a347fbd4 (diff)
downloadscalevalapokalypsi-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.scala173
-rw-r--r--src/main/scala/Server/Clients.scala82
-rw-r--r--src/main/scala/Server/ConnectionGetter.scala25
-rw-r--r--src/main/scala/Server/Server.scala195
-rw-r--r--src/main/scala/Server/constants.scala19
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