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

	/** Tests whether the client has behaved according to protocol.
	  *
	  * @return false if there has been a protocol violation, true otherwise
	  */
	def isIntactProtocolWise: Boolean = protocolIsIntact

	/** Tests whether this client is initialized and ready to start the game
	  *
	  * @return true if the client is ready to join the game
	  */
	def isReadyForGameStart: Boolean =
		this.protocolState == WaitingForGameStart

	/** Signals this client that it's joining the game. This is important so
	  * that this object knows to update its protocol state.
	  */
	def gameStart(): Unit = this.protocolState = InGame

	/** Returns the entity this client controls in the model.
	  *
	  * @return an option containing the entity
	  */
	def entity: Option[Entity] = this.character

	/** Tells this client object that it controls the specified entity.
	  *
	  * @param entity the entity this client is to control
	  */
	def giveEntity(entity: Entity): Unit =
		println(entity)
		this.character = Some(entity)

	/** Gets the name of this client, which should match the name of the entity
	  * that is given to this client. Not very useful if the client hasn't yet
	  * received the name or if it already has an entity.
	  *
	  * @return the name of this client
	  */
	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)
					.foreach(this.addDataToSend(_))
				true

end Client