package scalevalapokalypsi.Client import java.lang.Thread.sleep import java.net.Socket import scala.io.Source import scala.sys.process.stdout import scalevalapokalypsi.constants.* import scalevalapokalypsi.utils.{stringToByteArray,getNCharsFromSocket} import scalevalapokalypsi.Client.{ReceivedLineParser,StdinLineReader,Turn} import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global import scala.util.{Try, Success, Failure} import scala.collection.mutable.Buffer import java.lang.System.currentTimeMillis /** A helper enum for `Client` to keep track of communications with the server */ enum ServerLineState: case WaitingForTimeLimit, ActionsAndSong, TurnIndicator, AreaDescription, Directions, Items, Entities /** Creates a new client. * * @param name the name the client and its player should have * @param ip the ip of the server to connect to * @param port the port of the server to connect to * @return the client created, if all was successful */ def newClient(name: String, ip: String, port: Int): Option[Client] = val socket = Socket(ip, port) val output = socket.getOutputStream val input = socket.getInputStream val initMsg = s"$GAME_VERSION\r\n$name\r\n" output.write(stringToByteArray(initMsg)) val msgLen = (PROTOCOL_VERSION_GOOD + "\r\n").length val versionResponse = getNCharsFromSocket(input, msgLen) if versionResponse == Some(s"$PROTOCOL_VERSION_GOOD\r\n") then Some(Client(socket)) else None /** Main class for the client: handles communication with the server * and the player. Should be initialized with `newClient`. * * @param socket the socket the client uses */ class Client(socket: Socket): /** Essential IO variables */ private val input = socket.getInputStream private val output = socket.getOutputStream private val buffer: Array[Byte] = Array.ofDim(MAX_MSG_SIZE) private var bufferIndex = 0 private val serverLineParser = ReceivedLineParser() private val stdinReader = StdinLineReader() private var serverLineState = ServerLineState.WaitingForTimeLimit /** Variables about the status of the current turn for the client */ private var canAct = false // TODO: is really never true when it should private var timeLimit: Long = 0 private var lastTurnStart: Long = 0 private var lastExecutedTurn: Long = 0 private var isSinging: Boolean = false private val bufferedActions: Buffer[String] = Buffer.empty assert( lastTurnStart <= lastExecutedTurn, "don't initialize with unexecuted turn" ) private val turnInfo = Turn() /** Starts the client. This shouldn't terminate. */ def startClient(): Unit = stdinReader.startReading() while true do sleep(POLL_INTERVAL) this.readAndParseDataFromServer() this.displayActions() if this.lastExecutedTurn < this.lastTurnStart && !this.isSinging then print(this.giveTurn()) // TODO: we probably want to quit at EOF stdinReader.newLine().foreach((s: String) => println("not singing anymore!") this.isSinging = false output.write(stringToByteArray(s+"\r\n")) ) end startClient private def readAndParseDataFromServer(): Unit = var availableBytes = input.available() while availableBytes != 0 do val bytesRead = input.read(buffer, 0, availableBytes) if bytesRead != -1 then // TODO: unsafe conversion parseDataFromServer(buffer.take(bytesRead)) availableBytes = input.available() private def giveTurn(): String = this.canAct = true this.lastExecutedTurn = currentTimeMillis / 1000 s"\n\n${this.turnInfo}\n${this.actionGetterIndicator}" private def bufferAction(action: String): Unit = this.bufferedActions += action private def displayActions(): Unit = val somethingToShow = this.bufferedActions.nonEmpty if !this.isSinging then this.bufferedActions.foreach(println(_)) this.bufferedActions.clear() if !this.isSinging && this.canAct && somethingToShow then print(this.actionGetterIndicator) private def startSong(verse: String): Unit = this.isSinging = true print(s"\nLaula: “$verse”\n> ") private def actionGetterIndicator = val timeOfTurnEnd = this.lastTurnStart + this.timeLimit val timeToTurnEnd = -currentTimeMillis()/1000 + timeOfTurnEnd s"[$timeToTurnEnd]> " private def parseDataFromServer(data: Array[Byte]): Unit = this.serverLineParser.in(data) var nextLine: Option[String] = Some("") while nextLine.isDefined do nextLine = this.serverLineParser .nextLine() nextLine .foreach(this.parseLineFromServer(_)) private def parseLineFromServer(line: String) = if line == TURN_INDICATOR then this.serverLineState = ServerLineState.TurnIndicator serverLineState match case ServerLineState.WaitingForTimeLimit => val time = line.toLongOption time match case Some(t) => this.timeLimit = t case None => print("Invalid time limit, oh no!!!") this.serverLineState = ServerLineState.TurnIndicator this.lastTurnStart = currentTimeMillis / 1000 case ServerLineState.ActionsAndSong => if line.headOption.exists(_.toString == SING_INDICATOR) then this.startSong(line.tail) this.canAct = false else if line.headOption.contains(ACTION_BLOCKING_INDICATOR) then this.canAct = false this.bufferAction(line.tail) else if line.nonEmpty then this.bufferAction((line.tail)) else println("We should not get empty lines from the server!") case ServerLineState.TurnIndicator => this.serverLineState = ServerLineState.AreaDescription case ServerLineState.AreaDescription => this.turnInfo.areaDescription = line this.serverLineState = ServerLineState.Directions case ServerLineState.Directions => this.turnInfo.possibleDirections = line.split(LIST_SEPARATOR) this.serverLineState = ServerLineState.Items // TODO: maybe use a list instead? case ServerLineState.Items => this.turnInfo.visibleItems = line.split(LIST_SEPARATOR) this.serverLineState = ServerLineState.Entities case ServerLineState.Entities => this.turnInfo.visibleEntities = line.split(LIST_SEPARATOR) this.serverLineState = ServerLineState.ActionsAndSong this.lastTurnStart = currentTimeMillis() / 1000 end parseLineFromServer end Client