connection.clj

Branches > main > ea20cfa > 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 idempotently closed with ::close-fn, which returns the connection uuid, if it exists."
  [socket]
  (let [input-stream (Socket/.getInputStream socket)
        output-stream (Socket/.getOutputStream socket)
        state (atom {::phase/phase ::phase/handshaking})
        close (fn []
                (Socket/.close socket)
                (@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))