connection.clj
Branches > main > e001a8c > files / src / net / bowmap / transport / connection.clj
(ns net.bowmap.transport.connection
"A connection to the Minecraft server."
(:require
[clojure.core.async :refer [<!! chan close! io-thread put! timeout]]
[net.bowmap.protocol.packet :as pk]
[net.bowmap.protocol.packet.clientbound :as clientbound]
[net.bowmap.protocol.packet.serverbound :as serverbound]
[net.bowmap.protocol.state :as state]
[net.bowmap.server.player :as player]
[net.bowmap.transport.packets :refer [decode-packet! encode-packet!]]
[net.bowmap.transport.streams :as streams])
(:import
[java.io EOFException]
[java.net Socket SocketException]))
; TODO should all this decoding of packets be here instead?
; Connection state definitely should, so maybe packet stuff should as well
; But packets are protocol level stuff, so idk.
; The packets themselves are protocol-level, their parsing is network/transport level
; Or whatever I want to call this package.
; I think the barrier should be bytebuf readers and writers should be here, and the packets can use those functions
; Since they don't just describe serialization, but also schema and structure
(defn handle-serverbound-frame!
[state frame]
; TODO rename to protocol state, connection state is a misleading name
(let [{::state/keys! [connection-state]} @state
{::pk/keys! [id] :as packet} (decode-packet! frame connection-state)]
(case id
::serverbound/handshaking (swap! state assoc ::state/connection-state (req! packet :intent))
::serverbound/login-acknowledged (swap! state assoc ::state/connection-state ::state/config)
::serverbound/finish-config (swap! state assoc ::state/connection-state ::state/play)
::serverbound/login-start
(let [{:keys! [username uuid]} packet]
(swap! state assoc ::player/username username ::player/uuid uuid))
nil)
(let [{:keys [& ::player/uuid] :select player} @state]
(merge packet player))))
(defn connection
"Creates a connection object from a java.net.Socket
Connections have a channel of serverbound packets, and a channel of clientbound packets.
Connections can be closed with ::close-fn, and the promise at ::closed will deliver with
the connection's known uuid, or nil if the connection does not have one."
[socket]
(let [serverbound-packets (chan 64)
clientbound-packets (chan 64)
input-stream (Socket/.getInputStream socket)
output-stream (Socket/.getOutputStream socket)
state (atom {::state/connection-state ::state/handshaking})
closed-promise (promise)
conn {::close-fn (fn []
(Socket/.close socket)
(close! serverbound-packets)
(close! clientbound-packets))
::closed closed-promise
::serverbound-packets serverbound-packets
::clientbound-packets clientbound-packets}]
(io-thread
(loop []
(when (= ::not-closed (deref (promise) 0 ::not-closed))
(<!! (timeout 15000))
(when (#{::state/config ::state/play} (::state/connection-state @state))
(put! clientbound-packets
{::pk/id ::clientbound/keep-alive
:id (rand-int Integer/MAX_VALUE)}))
(recur))))
(io-thread
(try
(loop []
(put! serverbound-packets (handle-serverbound-frame! state (streams/read-frame!! input-stream)))
(recur))
(catch EOFException _)
(catch SocketException _)
(finally
(close! serverbound-packets)
(deliver closed-promise (@state ::player/uuid))
(Socket/.close socket))))
(io-thread
(try
(loop []
(when-some [packet (<!! clientbound-packets)]
(streams/write-frame!! (encode-packet! packet (req! @state ::state/connection-state)) output-stream)
(recur)))
(finally
(close! clientbound-packets))))
conn))