connection.clj

Branches > main > 2dca1d5 > 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))