package o1game.Client import java.lang.Thread.sleep import java.net.Socket import scala.io.Source import scala.io.StdIn.readLine import scala.sys.process.stdout import java.io.InputStream import o1game.constants.* import o1game.utils.stringToByteArray import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global import scala.util.{Try, Success, Failure} import scala.collection.mutable.Buffer import scala.collection.immutable.LazyList import java.lang.System.currentTimeMillis 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 def getNCharsFromSocket(input: InputStream, n: Int): Option[String] = val buffer: Array[Byte] = Array.ofDim(n) var i = 0 var failed = false while i < n && !failed do val res = input.read(buffer, i, n - i) if res < 0 then failed = true i += res // TODO: better error handling if failed then None else Some(String(buffer)) /** This class is for taking new lines from stdin when they are available. * reading starts when either newLine or clear or startReading are called. */ class StdinLineReader: private var nextLine: Future[String] = Future.failed(Exception()) /** Returns a new line of input if there are any. */ def newLine(): Option[String] = this.nextLine.value match case Some(Success(s)) => this.nextLine = Future(readLine()) Some(s) case Some(Failure(e)) => this.nextLine = Future(readLine()) None case None => None /** Discards the line that is currently being read and restarts reading */ def clear(): Unit = this.nextLine = Future(readLine()) /** Equivalent to clear */ def startReading(): Unit = this.clear() end StdinLineReader enum ServerLineState: case WaitingForGameStart, ActionDescription, TurnIndicator, AreaDescription, Directions, Items, Entities //def indexOfCRLF(data: IndexedSeq[Byte]): Int = // val LF = data.indexOf(10) // data.get(LF + 1).filter(_ == 13).filter(a => LF == -1).getOrElse(-1) // //def splitAtCRLF(data: IndexedSeq[Byte]): Vector[] class ServerDataParser: private var serverLineState = ServerLineState.ActionDescription private var bufferedData: Buffer[Byte] = Buffer.empty // TODO: suboptimal DS def in(data: Array[Byte]): Unit = this.bufferedData ++= data def nextLine(): Option[String] = val indexOfCRLF = this.bufferedData.indexOfSlice(CRLF) if indexOfCRLF == -1 then None else val splitData = this.bufferedData.splitAt(indexOfCRLF) this.bufferedData = Buffer.from(splitData(1).drop(CRLF.length)) Some(String(splitData(0).toArray)) end ServerDataParser class Client(socket: Socket): 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 var serverLineState = ServerLineState.WaitingForGameStart private val serverLineParser = ServerDataParser() private val stdinReader = StdinLineReader() private var timeLimit: Long = 0 private var lastTurnStart: Long = 0 private var lastExecutedTurn: Long = 0 assert( lastTurnStart <= lastExecutedTurn, "don't initialize with unexecuted turn" ) // TODO: extract these to a separate area object for the client private var actions: Buffer[String] = Buffer.empty private var areaDescription: String = "" private var possibleDirections: Array[String] = Array.empty private var visibleItems: Array[String] = Array.empty private var visibleEntities: Array[String] = Array.empty 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.lastExecutedTurn = currentTimeMillis / 1000 val actionDesc = this.actions.mkString("\n") this.actions = Buffer.empty val itemDesc = "You can see the following items: " + this.visibleItems.mkString(", ") val entityDesc = "The following entities reside in the room: " + this.visibleEntities.mkString(", ") val directionDesc = "There are exits to " + this.possibleDirections.mkString(", ") (s"\n$actionDesc\n\n$areaDescription\n$directionDesc\n" + s"\n$itemDesc\n$entityDesc") def startClient(): Unit = stdinReader.startReading() // TODO: read data from server and store it in the turn description // TODO: if turn isn't executed since lastturnstart, display the data // and clean STDIN // TODO: write data from stdin and send it to the server // TODO: display timer to next turn end while true do sleep(POLL_INTERVAL) this.readAndParseDataFromServer() if this.lastExecutedTurn < this.lastTurnStart then println(this.giveTurn()) stdinReader.newLine().foreach((s: String) => output.write(stringToByteArray(s+"\r\n")) ) if this.timeLimit != 0 && this.lastTurnStart != 0 then val timeOfTurnEnd = this.lastTurnStart + this.timeLimit val timeToTurnEnd = -currentTimeMillis()/1000 + timeOfTurnEnd print(s"\r[$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.WaitingForGameStart => val time = line.toLongOption time match case Some(t) => this.timeLimit = t case None => print("Invalid time limit, oh no!!!") this.serverLineState = ServerLineState.ActionDescription case ServerLineState.ActionDescription => this.actions.append(line) case ServerLineState.TurnIndicator => this.serverLineState = ServerLineState.AreaDescription case ServerLineState.AreaDescription => this.areaDescription = line this.serverLineState = ServerLineState.Directions case ServerLineState.Directions => this.possibleDirections = line.split(LIST_SEPARATOR) this.serverLineState = ServerLineState.Items // TODO: maybe use a list instead? case ServerLineState.Items => this.visibleItems = line.split(LIST_SEPARATOR) this.serverLineState = ServerLineState.Entities case ServerLineState.Entities => this.visibleEntities = line.split(LIST_SEPARATOR) this.serverLineState = ServerLineState.ActionDescription this.lastTurnStart = currentTimeMillis() / 1000 //bufferIndex = s"Houston, I think this shouldn't be so hard.\n".toVector.map(_.toByte).copyToArray(buffer) //output.write(buffer, 0, bufferIndex) //output.flush()