blob: 323b78c6c9cc44caef9b6fbdb270aa83f4910da9 (
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
|
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
private var nextAction: Option[Action] = 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 = 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 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 =
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"
/** 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 + 1)
this.incompleteMessage = rest ++ Array.fill(nextCRLF + 1)(0.toByte)
// TODO: the conversion may probably be exploited to crash the server
Some(String(message))
else
None
/** Makes the client play its turn */
def act(): Unit =
this.addDataToSend(ACTION_BLOCKING_INDICATOR.toString)
this.nextAction.foreach(this.executeAction(_))
this.nextAction = None
/** Checks whether the client has chosen its next action
*
* @return whether the client is ready to act */
def isReadyToAct: Boolean = this.nextAction.isDefined
/** 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\r\n")
this.protocolState = WaitingForClientName
true
else
addDataToSend(s"$PROTOCOL_VERSION_BAD\r\n")
false
case WaitingForClientName =>
this.name = Some(line)
this.protocolState = WaitingForGameStart
true
case WaitingForGameStart => true
case InGame =>
this.bufferAction(Action(line))
true
/** Buffers the action for execution or executes it immediately if it
* doesn't take a turn */
private def bufferAction(action: Action) =
if (
this.nextAction.isEmpty &&
this.entity.exists(action.takesATurnFor(_))
) then
this.nextAction = Some(action)
else if this.nextAction.isEmpty then
this.addDataToSend(ACTION_NONBLOCKING_INDICATOR.toString)
this.executeAction(action)
/** Executes the specified action and buffers its description for sending */
private def executeAction(action: Action) =
this.character.flatMap(action.execute(_)) match
case Some(s) => this.addDataToSend(s"$s\r\n")
case None => this.addDataToSend("You can't do that\r\n")
end Client
|