package scalevalapokalypsi.Server import java.net.Socket import scala.math.{min,max} import scalevalapokalypsi.constants.* import scalevalapokalypsi.utils.* import ServerProtocolState.* import scalevalapokalypsi.Model.Action import scalevalapokalypsi.Model.Entities.Player import java.lang.System.currentTimeMillis import scala.collection.mutable.Buffer // Note to graders etc // This class has an interesting design choice: // It does not write data directly to the client. // This is because with some multithreaded implementation of // Server.scala it could lead to race conditions etc where // several writes happened at once. // // Thus, this class never writes to the client anything, and // all TCP communication is the responsibility of the server. // Because of lack of time though, I had to rely on a very dirty // trick - the client has a workaround way to actually write data // to the client by queuing it to the server via the method // `dataToThisClient`. 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 turnUsed = false private var singStartTime: Option[Long] = None private var versesToSing: Vector[String] = Vector.empty private val versesSung: Buffer[String] = Buffer.empty private var verseIndex = 0 def clientHasSong = this.singStartTime.isDefined def startSongIfNeeded(): Unit = if this.player.exists(_.isSinging) && !this.clientHasSong then val verses = this.player.flatMap(_.getVerses) verses.foreach(v => this.versesToSing = v this.verseIndex = 0 this.singStartTime = Some(currentTimeMillis() / 1000) this.startVerse() ) /** Starts the next verse for the remote client, * use only when you have checked that there are verses left to sing! */ def startVerse(): Unit = val verse = this.versesToSing.lift(this.verseIndex) verse.foreach(v => this.addDataToSend(s"${SING_INDICATOR}$v") ) this.verseIndex += 1 /** Calculates the amount of bytes available for future incoming messages */ def spaceAvailable: Int = this.incompleteMessage.size - incompleteMessageIndex - 1 /** 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 = if data.length > this.spaceAvailable then false else for i <- 0 until min(data.length, this.spaceAvailable) do this.incompleteMessage(this.incompleteMessageIndex+i) = data(i) this.incompleteMessageIndex += data.length true /** 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) this.incompleteMessageIndex = 0 byteArrayToString(message) else None /** Makes the client play its turn */ def giveTurn(): Unit = this.turnUsed = false /** Checks whether the client has chosen its next action * * @return whether the client is ready to act */ def hasActed: Boolean = this.turnUsed /** 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.executeLine(line) true /** Buffers the action for execution or executes it immediately if it * doesn't take a turn */ private def executeLine(line: String) = if !this.turnUsed then this.singStartTime match case Some(t) => if this.verseIndex == this.versesToSing.length then val timeQuality = 5.0*this.versesToSing.length / max(5.0, currentTimeMillis()/1000 - t) val songToSing = this.versesToSing.mkString("") .toLowerCase val songSung = this.versesSung.mkString("").toLowerCase val quality = timeQuality * ( 1.0 - hammingDistance(songToSing, songSung) .toFloat / songToSing.length.toFloat ) this.player.foreach(_.applySingEffect(quality.toFloat)) this.singStartTime = None else this.versesSung += line this.startVerse() case None if isPrintable(line) => val action = Action(line) val takesATurn = this.character.exists(p => action.execute(p)) if takesATurn then this.addDataToSend(s"$ACTION_BLOCKING_INDICATOR") this.turnUsed = true case None => () // There were some illegal chars but whatever end executeLine end Client