aboutsummaryrefslogtreecommitdiff
path: root/src/scalevalapokalypsi/Server/Client.scala
diff options
context:
space:
mode:
Diffstat (limited to 'src/scalevalapokalypsi/Server/Client.scala')
-rw-r--r--src/scalevalapokalypsi/Server/Client.scala173
1 files changed, 173 insertions, 0 deletions
diff --git a/src/scalevalapokalypsi/Server/Client.scala b/src/scalevalapokalypsi/Server/Client.scala
new file mode 100644
index 0000000..6ce2522
--- /dev/null
+++ b/src/scalevalapokalypsi/Server/Client.scala
@@ -0,0 +1,173 @@
+package scalevalapokalypsi.Server
+
+import java.net.Socket
+import scala.math.min
+import scalevalapokalypsi.constants.*
+import ServerProtocolState.*
+import scalevalapokalypsi.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
+