package o1game.Server import java.net.Socket import scala.math.min import o1game.constants.* import ServerProtocolState.* import o1game.Model.Entity import o1game.Model.Action 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[Entity] = None private var protocolIsIntact = true private var name: Option[String] = 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 = protocolIsIntact /** 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 entity this client controls in the model. * * @return an option containing the entity */ def entity: Option[Entity] = this.character /** Tells this client object that it controls the specified entity. * * @param entity the entity this client is to control */ def giveEntity(entity: Entity): Unit = println(entity) this.character = Some(entity) /** Gets the name of this client, which should match the name of the entity * that is given to this client. Not very useful if the client hasn't yet * received the name or if it already has an entity. * * @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\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] = val nextLF = this.incompleteMessage.indexOf(LF) if nextLF != -1 then val message = this.incompleteMessage.take(nextLF) val rest = this.incompleteMessage.drop(nextLF + 1) this.incompleteMessage = rest ++ Array.fill(nextLF + 1)(0.toByte) // TODO: the conversion may probably be exploited to crash the server Some(String(message)) else None /** Causes the client to take the actions it has received */ def interpretData(): Unit = LazyList.continually(this.nextLine()) .takeWhile(_.isDefined) .flatten .foreach(s => takeAction(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 takeAction(line: String): Unit = this.protocolIsIntact = this.protocolState match case WaitingForVersion => if line == GAME_VERSION then addDataToSend(PROTOCOL_VERSION_GOOD) this.protocolState = WaitingForClientName true else addDataToSend(PROTOCOL_VERSION_BAD) false case WaitingForClientName => this.name = Some(line) this.protocolState = WaitingForGameStart true case WaitingForGameStart => true case InGame => println(line) val action = Action(line) this.character.flatMap(action.execute(_)) match case Some(s) => this.addDataToSend(s) case None => this.addDataToSend("You can't do that") this.character .map(_.location.fullDescription) .foreach(this.addDataToSend(_)) true end Client