diff options
Diffstat (limited to 'src/scalevalapokalypsi/Server/Client.scala')
-rw-r--r-- | src/scalevalapokalypsi/Server/Client.scala | 173 |
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 + |