aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/Server/Client.scala
blob: 1e5dd000819af9b75255ab3a4cf78750f7f59674 (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
package o1game.Server

import java.net.Socket
import scala.math.min
import o1game.constants.*
import ServerProtocolState.*
import o1game.Model.Entity
import o1game.Model.Action

class Client(val socket: Socket):
	private var incompleteMessage: Array[Byte] =
		Array.fill(MAX_MSG_SIZE)(0.toByte)
	private var incompleteMessageIndex = 0
	private var protocolState = WaitingForVersion
	private var outData: String = ""
	private var character: Option[Entity] = None
	private var protocolIsIntact = true
	private var name: Option[String] = None

	/** Calculates the amount of bytes available for future incoming messages
	 */
	def spaceAvailable: Int = MAX_MSG_SIZE - incompleteMessageIndex

	def isIntactProtocolWise: Boolean = protocolIsIntact
	def isReadyForGameStart: Boolean =
		this.protocolState == WaitingForGameStart
	def gameStart(): Unit = this.protocolState = InGame
	def entity: Option[Entity] = this.character
	def giveEntity(entity: Entity): Unit =
		println(entity)
		this.character = Some(entity)
	def getName: Option[String] = this.name

	/** Sets `data` as received for the client.
	 *
	 *  @return false means there was not enough space to receive the message
	 */
	def receiveData(data: Vector[Byte]): Boolean =
		for i <- 0 until min(data.length, spaceAvailable) do
			this.incompleteMessage(this.incompleteMessageIndex + i) = data(i)
		this.incompleteMessageIndex += data.length
		this.incompleteMessageIndex =
			min(this.incompleteMessageIndex, MAX_MSG_SIZE)
		data.length < spaceAvailable

	/** Returns data that should be sent to this client.
	 *  The data is cleared when calling.
	 */
	def dataToThisClient(): String =
		val a = this.outData
		this.outData = ""
		a

	/** Specifies that the data should be buffered for
	 *  sending to this client
	 *
	 * @param data data to buffer for sending
	 */
	private def addDataToSend(data: String): Unit =
		this.outData += s"$data\n"


	/** Returns one line of data if there are any line breaks.
	 *  Removes the parsed data from the message buffering area.
	 */
	private def nextLine(): Option[String] =
		val nextLF = this.incompleteMessage.indexOf(LF)
		if nextLF != -1 then
			val message = this.incompleteMessage.take(nextLF)
			val rest = this.incompleteMessage.drop(nextLF + 1)
			this.incompleteMessage = rest ++ Array.fill(nextLF + 1)(0.toByte)
			// TODO: the conversion may probably be exploited to crash the server
			Some(String(message))
		else
			None

	/** Causes the client to take the actions it has received
	 */
	def interpretData(): Unit =
		LazyList.continually(this.nextLine())
			.takeWhile(_.isDefined)
			.flatten
			.foreach(s => takeAction(s))

	/** Makes the client execute the action specified by `line`.
	 *  If there is a protocol error, the function changes
	 *  the variable `protocolIsIntact` to false.
	 *
	 * @param line the line to interpret
	 */
	private def takeAction(line: String): Unit =
		this.protocolIsIntact = this.protocolState match
			case WaitingForVersion =>
				if line == GAME_VERSION then
					addDataToSend(PROTOCOL_VERSION_GOOD)
					this.protocolState = WaitingForClientName
					true
				else
					addDataToSend(PROTOCOL_VERSION_BAD)
					false
			case WaitingForClientName =>
				this.name = Some(line)
				this.protocolState = WaitingForGameStart
				true
			case WaitingForGameStart => true
			case InGame =>
				println(line)
				val action = Action(line)
				this.character.flatMap(action.execute(_)) match
					case Some(s) => this.addDataToSend(s)
					case None => this.addDataToSend("You can't do that")
				this.character.map(_.location.fullDescription) match
					case Some(s) => this.addDataToSend(s)
				true

end Client