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
|