aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-04 21:38:50 +0200
committerJoel Kronqvist <joel.kronqvist@iki.fi>2024-11-04 22:03:40 +0200
commitae82027a9bd4e75582f9499d4006b18c29a4129c (patch)
tree92d23012158647d31fbdc0e6cdae7854ee773cbc
downloadscalevalapokalypsi-ae82027a9bd4e75582f9499d4006b18c29a4129c.tar.gz
scalevalapokalypsi-ae82027a9bd4e75582f9499d4006b18c29a4129c.zip
Added basic TCP networking for the server.
The client's networking is still very experimental and the actual protocol is not yet specified for either side.
-rw-r--r--.gitignore7
-rw-r--r--src/main/scala/Client/Client.scala28
-rw-r--r--src/main/scala/Server/Character.scala7
-rw-r--r--src/main/scala/Server/Client.scala80
-rw-r--r--src/main/scala/Server/Server.scala90
-rw-r--r--src/main/scala/Server/constants.scala6
-rw-r--r--src/main/scala/main.scala15
7 files changed, 233 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a114ced
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.bsp
+.idea
+build.sbt
+project
+target
+src/main/scala/.bsp
+src/main/scala/.scala-build
diff --git a/src/main/scala/Client/Client.scala b/src/main/scala/Client/Client.scala
new file mode 100644
index 0000000..1b843ab
--- /dev/null
+++ b/src/main/scala/Client/Client.scala
@@ -0,0 +1,28 @@
+package o1game.Client
+
+import java.lang.Thread.sleep
+import java.net.Socket
+import scala.io.Source
+import scala.sys.process.stdout
+import o1game.constants.*
+
+class Client(ip: String, port: Int):
+ private val socket = Socket(ip, port)
+ private val input = socket.getInputStream
+ private val output = socket.getOutputStream
+ private val buffer: Array[Byte] = Array.ofDim(MAX_MSG_SIZE)
+ private var bufferIndex = 0
+
+ def startClient(): Unit =
+ while true do
+ sleep(POLL_INTERVAL)
+
+ while input.available() != 0 do
+ val bytesRead = input.read(buffer)
+ if bytesRead != -1 then
+ print(buffer.take(bytesRead).toVector.map(_.toChar).mkString)
+ stdout.flush()
+
+ bufferIndex = s"Houston, I think this shouldn't be so hard.\n".toVector.map(_.toByte).copyToArray(buffer)
+ output.write(buffer, 0, bufferIndex)
+ output.flush() \ No newline at end of file
diff --git a/src/main/scala/Server/Character.scala b/src/main/scala/Server/Character.scala
new file mode 100644
index 0000000..082f152
--- /dev/null
+++ b/src/main/scala/Server/Character.scala
@@ -0,0 +1,7 @@
+package o1game.Server
+
+import scala.collection.mutable.Buffer
+
+class GameCharacter(val name: String, initialItems: Iterable[String]):
+ private val items: Buffer[String] = Buffer.from(items) // TODO: Item class
+ // TODO: A lot of other things too
diff --git a/src/main/scala/Server/Client.scala b/src/main/scala/Server/Client.scala
new file mode 100644
index 0000000..c7ff075
--- /dev/null
+++ b/src/main/scala/Server/Client.scala
@@ -0,0 +1,80 @@
+package o1game.Server
+
+import java.net.Socket
+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
+
+ def allClients: Iterable[Client] = clients.toVector.flatten
+ def foreach(f: Client => Any): Unit = this.clients.flatten.foreach(f)
+ 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 => \ No newline at end of file
diff --git a/src/main/scala/Server/Server.scala b/src/main/scala/Server/Server.scala
new file mode 100644
index 0000000..829010c
--- /dev/null
+++ b/src/main/scala/Server/Server.scala
@@ -0,0 +1,90 @@
+package o1game.Server
+
+
+// TODO: TLS/SSL / import javax.net.ssl.SSLServerSocketFactory
+
+import java.io.IOException
+import java.lang.Thread.sleep
+import java.net.{ServerSocket, Socket}
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.Future
+import scala.util.{Failure, Success, Try}
+import o1game.constants.*
+
+/** `Server` exists to initialize a server for the game
+ * and run it with its method `startServer`.
+ */
+class Server(port: Int, maxClients: Int):
+ private val socket = ServerSocket(port)
+ private val clientGetter = ConnectionGetter(socket)
+ private val clients: Clients = Clients(maxClients)
+ private val buffer: Array[Byte] = Array.ofDim(1024)
+ private var bufferIndex = 0
+
+ /** Starts the server. Won't terminate under normal circumstances. */
+ def startServer(): Unit =
+ while true do
+ sleep(POLL_INTERVAL)
+ this.receiveNewClient()
+ this.writeToAll("Test message.")
+ this.readFromAll()
+ clients.foreach(_.executeActions())
+
+ /** Receives a new client and stores it in `clients`.
+ *
+ * @return describes if a client was added
+ */
+ private def receiveNewClient(): Boolean =
+ clientGetter.newClient() match
+ case Some(c) =>
+ clients.addClient(Client(c, None))
+ true
+ case None =>
+ false
+
+ /** Sends `message` to all clients
+ *
+ * @param message the message to send
+ */
+ private def writeToAll(message: String): Unit =
+ clients.removeNonSatisfying((c: Client) =>
+ try
+ val output = c.socket.getOutputStream
+ output.write(message.toVector.map(_.toByte).toArray)
+ output.flush()
+ true
+ catch
+ case e: IOException => false
+ )
+
+ /** Reads data sent by clients and stores it in the `Client`s of `clients` */
+ private def readFromAll(): Unit =
+ clients.removeNonSatisfying((c: Client) =>
+ try
+ val input = c.socket.getInputStream
+ while input.available() != 0 do
+ val bytesRead = input.read(buffer)
+ if bytesRead != -1 then
+ c.receiveData(buffer.take(bytesRead).toVector)
+ true
+ catch
+ case e: IOException => false
+ )
+
+end Server
+
+class ConnectionGetter(val socket: ServerSocket):
+
+ private var nextClient: Future[Socket] = Future.failed(IOException())
+
+ def newClient(): Option[Socket] =
+ this.nextClient.value match
+ case Some(Success(s)) =>
+ nextClient = Future(socket.accept())
+ Some(s)
+ case Some(Failure(e)) =>
+ nextClient = Future(socket.accept())
+ None
+ case None => None
+
+end ConnectionGetter
diff --git a/src/main/scala/Server/constants.scala b/src/main/scala/Server/constants.scala
new file mode 100644
index 0000000..4260a60
--- /dev/null
+++ b/src/main/scala/Server/constants.scala
@@ -0,0 +1,6 @@
+
+package o1game.constants
+
+val MAX_MSG_SIZE = 1024 // bytes
+val LF: Byte = 10
+val POLL_INTERVAL = 100 // millisec.
diff --git a/src/main/scala/main.scala b/src/main/scala/main.scala
new file mode 100644
index 0000000..18172e2
--- /dev/null
+++ b/src/main/scala/main.scala
@@ -0,0 +1,15 @@
+
+import o1game.Client.Client
+import o1game.Server.Server
+
+import scala.io.StdIn.readLine
+
+// TODO: add proper logic for starting the game
+@main def main(): Unit =
+ print("Please choose:\n1) Client.Client\n2) Server\n> ")
+ readLine().toIntOption match
+ case Some(1) => Client("127.0.0.1", 2267).startClient()
+ case Some(2) => Server(2267, 5).startServer()
+ case _ => println("Invalid input")
+
+