connection.clj
Branches > main > 315fbaa > files / src / net / bowmap / transport / connection.clj
(ns net.bowmap.transport.connection
"A connection to the Minecraft server."
(:require
[clojure.core.async :refer [io-thread]]
[net.bowmap.protocol.packet :as pk]
[net.bowmap.protocol.packet.clientbound :as clientbound]
[net.bowmap.protocol.packet.serverbound :as serverbound]
[net.bowmap.server.player :as player]
[net.bowmap.transport.packets :refer [decode-packet! encode-packet!]]
[net.bowmap.transport.phase :as phase]
[net.bowmap.transport.streams :as streams])
(:import
[java.io EOFException]
[java.net Socket SocketException]))
(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 [input-stream (Socket/.getInputStream socket)
output-stream (Socket/.getOutputStream socket)
state (atom {::phase/phase ::phase/handshaking})
closed-promise (promise)
close (fn []
(Socket/.close socket)
(deliver closed-promise (@state ::player/uuid))
(@state ::player/uuid))
conn {::close-fn close
::send-packet (fn [packet]
(when (Socket/.isClosed socket)
(throw (ex-info "Tried to send packet to a disconnected client" {:packet packet})))
(try
(streams/write-frame!! (encode-packet! packet (req! @state ::phase/phase)) output-stream)
(catch Throwable t
(close)
(when-not (instance? SocketException t)
(throw t)))))
::receive-packet (fn []
(when (Socket/.isClosed socket)
(throw (Exception. "Tried to read packet from a disconnected client")))
(try
(let [frame (streams/read-frame!! input-stream)
{::phase/keys! [phase]} @state
packet (decode-packet! frame phase)]
(when-let [{::pk/keys! [id]} packet]
(case id
::serverbound/handshaking (swap! state assoc ::phase/phase (req! packet ::phase/phase))
::serverbound/login-acknowledged (swap! state assoc ::phase/phase ::phase/config)
::serverbound/finish-config (swap! state assoc ::phase/phase ::phase/play)
::serverbound/login-start
(let [{:keys! [username uuid]} packet]
(swap! state assoc ::player/uuid uuid))
nil)
(merge packet (select-keys @state [::player/uuid]))))
(catch Throwable t
(close)
(when-not (or (instance? SocketException t) (instance? EOFException t))
(throw t)))))}]
(io-thread
(loop []
(Thread/sleep 15000)
(when (#{::phase/config ::phase/play} (::phase/phase @state))
((::send-packet conn)
{::pk/id ::clientbound/keep-alive
:id (rand-int Integer/MAX_VALUE)}))
(recur)))
conn))