connection.clj

Branches > main > 3079540 > 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.transport.phase :as phase]
   [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]))

(defn handle-serverbound-frame!
  [state conn frame]
  (let [{::phase/keys! [phase]} @state
        packet (try (decode-packet! frame phase)
                    (catch Exception e
                      (Throwable/.printStackTrace (Exception. "Failed to decode packet!" e))
                      nil))]
    (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/username username ::player/uuid uuid))
        nil)
      ; Just kinda crudely passing the connection into the serverbound packet right now
      (merge packet {::connection conn} (select-keys @state [::player/uuid])))))

(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 {::phase/phase ::phase/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 (#{::phase/config ::phase/play} (::phase/phase @state))
            (put! clientbound-packets
                  {::pk/id ::clientbound/keep-alive
                   :id (rand-int Integer/MAX_VALUE)}))
          (recur))))

    (io-thread
      (try
        (loop []
          (when-let [packet (handle-serverbound-frame! state conn (streams/read-frame!! input-stream))]
            (put! serverbound-packets packet))
          (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 ::phase/phase)) output-stream)
            (recur)))
        (catch SocketException _)
        (finally
          (close! clientbound-packets))))
    conn))