aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/Client/Client.scala
blob: ef98ef35af4b7338fcf61efca31d580b8a168b36 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
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()