package scalevalapokalypsi.Client import java.net.{Socket,InetSocketAddress} import scala.io.Source import scala.sys.process.stdout import scalevalapokalypsi.constants.* import scalevalapokalypsi.utils.{stringToByteArray,getNCharsFromSocket} import scalevalapokalypsi.Client.{ReceivedLineParser,RoomState} 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() socket.connect(new InetSocketAddress(ip, port), INITIAL_CONN_TIMEOUT) 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 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 lineToSing: Option[String] = None private val bufferedActions: Buffer[String] = Buffer.empty assert( lastTurnStart <= lastExecutedTurn, "don't initialize with unexecuted turn" ) private val turnInfo = RoomState() private var gameOver = false /** Takes a client step and optionally returns an in-game event for UI * * @param clientInput one line of client input if any * @return an event describing new changes in the game state */ def clientStep(clientInput: Option[String]): GameEvent = this.readAndParseDataFromServer() val actions = this.getNewActions() val roomState = if this.lastExecutedTurn < this.lastTurnStart && this.lineToSing.isEmpty then this.giveTurn() Some(this.turnInfo) else None for line <- clientInput do this.lineToSing = None output.write(stringToByteArray(s"$line\r\n")) val timeOfTurnEnd = this.lastTurnStart + this.timeLimit val timeToTurnEnd = -currentTimeMillis()/1000 + timeOfTurnEnd GameEvent( actions, roomState, this.lineToSing, this.canAct, Some(timeToTurnEnd).filter(p => this.lastTurnStart != 0), !this.gameOver ) end clientStep private def readAndParseDataFromServer(): Unit = var availableBytes = input.available() while availableBytes != 0 do val bytesRead = input.read(buffer, 0, availableBytes) if bytesRead != -1 then parseDataFromServer(buffer.take(bytesRead)) availableBytes = input.available() private def giveTurn(): Unit = this.canAct = true this.lastExecutedTurn = currentTimeMillis / 1000 private def bufferAction(action: String): Unit = this.bufferedActions += action private def getNewActions(): Option[Vector[String]] = val somethingToShow = this.bufferedActions.nonEmpty if somethingToShow && this.lineToSing.isEmpty then val res = this.bufferedActions.toVector this.bufferedActions.clear() Some(res) else None private def startSong(verse: String): Unit = this.lineToSing = Some(verse) 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 if line == GAME_END_INDICATOR then this.gameOver = true 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) 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)) case ServerLineState.TurnIndicator => this.serverLineState = ServerLineState.AreaDescription case ServerLineState.AreaDescription => this.turnInfo.areaDescription = line this.serverLineState = ServerLineState.Directions case ServerLineState.Directions => val dirs = line.split(LIST_SEPARATOR) if dirs(0) == "" && dirs.length == 1 then this.turnInfo.possibleDirections = Array.empty else this.turnInfo.possibleDirections = dirs this.serverLineState = ServerLineState.Items 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