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

import java.net.Socket
import scala.util.Try
import scala.math.min
import o1game.constants.*

object Client:
	def parseClient(data: String, socket: Socket): Client =
		Client(socket, Some(GameCharacter(data, Vector())))

class Client(val socket: Socket, val character: Option[GameCharacter]):
	private var incompleteMessage: Array[Byte] =
		Array.fill(MAX_MSG_SIZE)(0.toByte)
	private var incompleteMessageIndex = 0

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

	/** 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 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 executeActions(): Unit =
		LazyList.continually(this.nextLine())
			.takeWhile(_.isDefined)
			.flatten
			.foreach(s => println(s"`$this` executing `$s`"))


class Clients(maxClients: Int):
	private val clients: Array[Option[Client]] = Array.fill(maxClients)(None)

	/** Adds `client` to this collection of clients.
	 *
	 * @param client the Client to add
	 * @return true if there was room for the client
	 *         i.e. fewer clients than `maxClients`, false otherwise
	 */
	def addClient(client: Client): Boolean =
		val i = this.clients.indexOf(None)
		if i == -1 then
			false
		else
			this.clients(i) = Some(client)
			true

	/** Returns all the clients.
	 *
	 * @return an iterable of all the clients
	 */
	def allClients: Iterable[Client] = clients.toVector.flatten

	/** Applies the function `f` to all the clients for its side effects. */
	def foreach(f: Client => Any): Unit = this.clients.flatten.foreach(f)

	/** Applies the function `f` to all the clients for its side effects
	 *  and removes all the clients for which `f([client])` returns false.
	 *  This is useful for doing IO with the client and removing clients
	 *  with stale sockets.
	 *
	 * @param f the function to apply to all the clients and filter them with
	 */
	def removeNonSatisfying(f: Client => Boolean): Unit =
		for i <- this.clients.indices do
			this.clients(i) match
				case Some(c) =>
					if !f(c) then
						this.clients(i) = None
				case None =>

	/** Applies the function f to all clients for its side effects.
	 *  If the function throws an exception, the client is removed.
	 *  Probably a more concise alternative to `removeNonSatisfying`,
	 *  but might catch exceptions unintentionally.
	 *
	 * @param f the function to apply for its side effects to each client
	 */
	def mapAndRemove(f: Client => Unit): Unit =
		this.removeNonSatisfying(c => Try(f(c)).isSuccess)