diff options
Diffstat (limited to 'src/main/scala/Client/Client.scala')
-rw-r--r-- | src/main/scala/Client/Client.scala | 206 |
1 files changed, 85 insertions, 121 deletions
diff --git a/src/main/scala/Client/Client.scala b/src/main/scala/Client/Client.scala index 2503cdf..e9d3074 100644 --- a/src/main/scala/Client/Client.scala +++ b/src/main/scala/Client/Client.scala @@ -3,69 +3,19 @@ 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 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 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. +/** A helper enum for `Client` to keep track of communications with the server */ -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, @@ -76,81 +26,60 @@ enum ServerLineState: 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 +/** 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 - 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 +/** 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 var serverLineState = ServerLineState.WaitingForGameStart - private val serverLineParser = ServerDataParser() + private val serverLineParser = ReceivedLineParser() private val stdinReader = StdinLineReader() - private var timeLimit: Long = 0 + private var serverLineState = ServerLineState.WaitingForGameStart + + /** 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() - // 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\n") - + /** Starts the client. This shouldn't terminate. */ def startClient(): Unit = + stdinReader.startReading() while true do @@ -165,10 +94,34 @@ class Client(socket: Socket): 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]> ") + 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(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) @@ -179,35 +132,46 @@ class Client(socket: Socket): 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) + if line.head == ACTION_BLOCKING_INDICATOR then + this.canAct = false + this.displayAction(line.tail) + case ServerLineState.TurnIndicator => this.serverLineState = ServerLineState.AreaDescription + case ServerLineState.AreaDescription => - this.areaDescription = line + this.turnInfo.areaDescription = line this.serverLineState = ServerLineState.Directions + case ServerLineState.Directions => - this.possibleDirections = line.split(LIST_SEPARATOR) + this.turnInfo.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.turnInfo.visibleItems = line.split(LIST_SEPARATOR) this.serverLineState = ServerLineState.Entities + case ServerLineState.Entities => - this.visibleEntities = line.split(LIST_SEPARATOR) + this.turnInfo.visibleEntities = line.split(LIST_SEPARATOR) this.serverLineState = ServerLineState.ActionDescription this.lastTurnStart = currentTimeMillis() / 1000 + end parseLineFromServer - //bufferIndex = s"Houston, I think this shouldn't be so hard.\n".toVector.map(_.toByte).copyToArray(buffer) - //output.write(buffer, 0, bufferIndex) - //output.flush() +end Client |