minecraft_server.clj
Branches > main > 39a68d5 > files / src / net / bowmap / minecraft_server.clj
(ns net.bowmap.minecraft-server
(:require
[clojure.core.async :refer [<!! >!! alt!! chan close! io-thread put!
timeout]]
[net.bowmap.message-application :as a]
[net.bowmap.network.client :as client]
[net.bowmap.network.protocol.message.clientbound :as clientbound]
[net.bowmap.network.protocol.message.serverbound :as serverbound]
[net.bowmap.network.protocol.state :as state]
[net.bowmap.network.server :as server]))
; The logical server, holds a set of clients connected
(defn run-effects! [messages-out process-chan result]
(let [{:keys! [::a/processes ::a/messages] ::client/keys! [client]} result]
(doseq [process processes]
(io-thread
(<!! (timeout (::a/process-timeout process)))
(>!! process-chan process)))
(doseq [msg messages]
(put! messages-out msg))
client))
(defn read-and-handle-packet!
"Reads/deserializes one packet into a message, and handles it.
Because packet reading is dependent on the state of the client, and handling a packet can update the state of the client,
Reading and handling must be synchronized!"
[pk client messages-out process-chan]
(when-some [msg (serverbound/packet->message pk (::state/connection-state client))]
(run-effects! messages-out process-chan (a/apply-message msg client))))
(defn handle-process! [process client messages-out process-chan]
(run-effects! messages-out process-chan (a/apply-process process client)))
; TODO I don't really like all these channels...
(defn handle-packets-and-messages! [client packets-in messages-out packets-out process-chan]
(alt!!
process-chan ([process]
(handle-process! process client messages-out process-chan))
messages-out ([msg]
(when msg
(assert (>!! packets-out (clientbound/message->packet msg (::state/connection-state client))))
client))
packets-in ([pk]
(when pk
(read-and-handle-packet! pk client messages-out process-chan)))))
(defn connect! [client packets-in packets-out]
(let [messages-out (chan 1024)
process-chan (chan 64)]
; TODO error handling and disconnect
(io-thread
(loop [client client]
(when-some [client (handle-packets-and-messages! client packets-in messages-out packets-out process-chan)]
; Otherwise, stop?
(recur client))))
nil))
(defn client
"Returns a new client value"
[]
{::state/connection-state ::state/handshaking})
(defn closable-promise
"Returns a promise that closes 'ch' when it is resolved."
[ch]
(let [p (promise)]
(io-thread
@p
(close! ch))
p))
; TODO run all of this through a var so it's reloadable, and clearly so
; TODO this is networking, and so should be in net.bowmap.server.network
(defn start-server!
"Starts the Minecraft server at the specified port.
Returns a promise that will deliver when the server closes.
Delivering the promise will also close the server."
[{:keys! [port]}]
(let [clients (server/start-tcp-server! {:port port})
shutdown (closable-promise clients)]
; Loop over the instance?
(io-thread
(loop []
(if-some [{::server/keys! [packets-in packets-out]} (<!! clients)]
(do (connect! (client) packets-in packets-out)
(recur))
(deliver shutdown :shutdown))))
shutdown))