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

import java.net.Socket
import scala.math.{min,max}
import scalevalapokalypsi.constants.*
import ServerProtocolState.*
import scalevalapokalypsi.Model.Action
import scalevalapokalypsi.Model.Entities.Player
import java.lang.System.currentTimeMillis

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[Player] = None
	private var protocolIsIntact = true
	private var name: Option[String] = None
	private var nextAction: Option[Action] = None
	private var turnUsed = false
	private var singStartTime: Option[Long] = None
	private var verseToSing: String = ""

	def clientHasSong = this.singStartTime.isDefined
	def startSong(verse: String): Unit =
		this.verseToSing = verse
		this.singStartTime = Some(currentTimeMillis() / 1000)

	/** Calculates the amount of bytes available for future incoming messages */
	def spaceAvailable: Int = this.incompleteMessage.size - incompleteMessageIndex - 1

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

	/** Marks that this client misbehaved in eyes of the protocol */
	def failedProtocol(): Unit = this.protocolIsIntact = false

	/** 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 player this client controls in the model.
	  *
	  * @return an option containing the player
	  */
	def player: Option[Player] = this.character

	/** Tells this client object that it controls the specified player.
	  *
	  * @param player the player this client is to control
	  */
	def givePlayer(player: Player): Unit =
		this.character = Some(player)

	/** Gets the name of this client, which should match the name of the player
	  * that is given to this client. Not very useful if the client hasn't yet
	  * received the name or if it already has an player.
	  *
	  * @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 =
		if data.length > this.spaceAvailable then
			false
		else
			for i <- 0 until min(data.length, this.spaceAvailable) do
				this.incompleteMessage(this.incompleteMessageIndex+i) = data(i)
			this.incompleteMessageIndex += data.length
			true

	/** 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\r\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] =
		var nextCRLF = this.incompleteMessage.indexOf(CRLF(0))
		if this.incompleteMessage(nextCRLF + 1) != CRLF(1) then nextCRLF = -1
		if nextCRLF != -1 then
			val message = this.incompleteMessage.take(nextCRLF)
			val rest = this.incompleteMessage.drop(nextCRLF + 2)
			this.incompleteMessage = rest ++ Array.fill(nextCRLF + 1)(0.toByte)
			this.incompleteMessageIndex = 0
			// TODO: the conversion may probably be exploited to crash the server
			Some(String(message))
		else
			None

	/** Makes the client play its turn */
	def giveTurn(): Unit =
		this.turnUsed = false

	/** Checks whether the client has chosen its next action
	  *
	  * @return whether the client is ready to act */
	def hasActed: Boolean = this.turnUsed

	/** Causes the client to interpret the data it has received */
	def interpretData(): Unit =
		LazyList.continually(this.nextLine())
			.takeWhile(_.isDefined)
			.flatten
			.foreach(s => interpretLine(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 interpretLine(line: String): Unit =
		this.protocolIsIntact = this.protocolState match
			case WaitingForVersion =>
				if line == GAME_VERSION then
					addDataToSend(s"$PROTOCOL_VERSION_GOOD")
					this.protocolState = WaitingForClientName
					true
				else
					addDataToSend(s"$PROTOCOL_VERSION_BAD")
					false
			case WaitingForClientName =>
				this.name = Some(line)
				this.protocolState = WaitingForGameStart
				true
			case WaitingForGameStart => true
			case InGame =>
				this.executeLine(line)
				true

	/** Buffers the action for execution or executes it immediately if it
	  * doesn't take a turn */
	private def executeLine(line: String) =

		if !this.turnUsed then
			this.singStartTime match
				case Some(t) =>

					val timePassed = currentTimeMillis()/1000 - t

					val quality = if line == this.verseToSing then
						5.0 / max(5.0, timePassed.toDouble)
					else
						0.0

					this.player.foreach(_.applySingEffect(quality.toFloat))

					this.singStartTime = None
					
				case None =>

					val action = Action(line)
					val takesATurn = this.character.exists(p => action.execute(p))
					if takesATurn then
						this.addDataToSend(s"$ACTION_BLOCKING_INDICATOR")
						this.turnUsed = true

	end executeLine

end Client