package scalevalapokalypsi.Server import java.net.Socket import scala.math.{min,max} import scalevalapokalypsi.constants.* import ServerProtocolState.* import scalevalapokalypsi.Model.Action import scalevalapokalypsi.Model.Entities.Player import java.lang.System.currentTimeMillis 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 private var singStartTime: Option[Long] = None def clientHasSong = this.singStartTime.isDefined def startSong(): Unit = this.singStartTime = Some(currentTimeMillis() / 1000) /** 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.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.singStartTime match case Some(t) => val timePassed = currentTimeMillis()/1000 - t this.player.foreach(_.applySingEffect( 5 / max(5, timePassed) )) this.singStartTime = None case None => 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