package o1game.Client import java.lang.Thread.sleep import java.net.Socket import scala.io.Source import scala.sys.process.stdout import o1game.constants.* import o1game.utils.{stringToByteArray,getNCharsFromSocket} import o1game.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, ActionDescription, TurnIndicator, AreaDescription, Directions, Items, Entities /** Creates a new client. * * @param name the name the client and its player should have * @ip the ip of the server to connect to * @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 private var timeLimit: Long = 0 private var lastTurnStart: Long = 0 private var lastExecutedTurn: Long = 0 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() if this.lastExecutedTurn < this.lastTurnStart then print(this.giveTurn()) stdinReader.newLine().foreach((s: String) => 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 displayAction(action: String): Unit = println(s"> $action") if this.canAct then print(this.actionGetterIndicator) 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.ActionDescription => if !line.isEmpty && line.head == ACTION_BLOCKING_INDICATOR then this.canAct = false this.displayAction(line.tail) 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.ActionDescription this.lastTurnStart = currentTimeMillis() / 1000 end parseLineFromServer end Client