client.clj

Branches > refactor/use-spec-1 > 92885b7 > files / src / net / bowmap / packets / client.clj

(ns net.bowmap.packets.client
  "Clientbound packets"
  (:require
   [clojure.alpha.spec :as s]
   [dev.grahp.error :refer [assert!]]
   [net.bowmap.client.state :as state]
   [net.bowmap.codecs.write :as write :refer [write-varint!]]
   [net.bowmap.packet :as pk :refer [packet?]]
   [net.bowmap.packets.registry :as reg])
  (:import
   [io.netty.buffer ByteBuf Unpooled]))

;; A lot of work to do in this file lol

;; TODO this also looks identical to the serverbound packet ids...
(defmacro build-packet-ids [registry]
  (into {}
        (mapcat (fn [[state packet-ids]]
                  (into {} (map-indexed (fn [i pk] [{::state/state state ::pk/id pk} i]))
                        packet-ids)))
        registry))

(defn build-packet [pk-name schema]
  (let [fn-name (symbol (str "encode-" (name pk-name) "-packet"))
        fields (map :field schema)
        types (map :type schema)
        codecs (map write/writer types)
        name-codec-pairs (for [codec codecs]
                           [(gensym) codec])
        codec-names (map first name-codec-pairs)
        bindings (apply concat name-codec-pairs)
        pairs? (map vector fields codec-names)
        buf (gensym "buf__")
        pk (gensym "pk__")]
    `(let [~@bindings]
       (fn ~fn-name [~buf ~pk]
         ~@(for [[field codec] pairs?]
             `(~codec ~buf (~field ~pk)))
         ~buf))))

(defmacro build-packets [registry]
  (reduce (fn [acc [packet-id schema]]
            (assoc acc packet-id (build-packet packet-id schema)))
          {} (reg/conform-registry registry)))

(def ^{:spec (s/map-of (s/schema [::state/state ::pk/id]) ::pk/numeric-id)}
  packet-ids
  "Map of [::state/state ::pk/id] to numeric-id.
   Packet ids (::pk/id) could map directly to numeric-ids and be stateless,
    but some packets have duplicates across connection states that you'd want to handle in the same way.
   If it turns out that I don't end up handling packets in this way, then packet ids could map directly to numeric ids."
  (build-packet-ids
   {::state/status
    [::status-response
     ::pong-response]
    ::state/login
    [::login-disconnect
     ::encryption-request
     ::login-success
     ::set-compression
     ::login-plugin-request
     ::login-cookie-request]}))

(defn numeric-id [state id]
  (packet-ids {::state/state state ::pk/id id}))

(def ^{:spec (s/map-of ::pk/id (s/fspec :args (s/cat :buf #(instance? ByteBuf %) :pk ::pk/packet)
                                        :ret #(instance? ByteBuf %)))}
  packets
  (build-packets
   {::status-response [:response "Response to ::sp/status-request" :json]
    ::pong-response [:timestamp "The same timestamp as ::sp/ping-request" :long]
    ::login-success [:uuid :uuid
                     :username :string
                     :properties [:list [:table [:name :string
                                                 :value :string
                                                 :signature [:optional :string]]]]]}))

(defn packet-encoder [pk]
  (packets (::pk/id pk)))

(defn encode-packet
  "Encodes packet 'pk' into a ByteBuf of the packet bytes."
  [pk state]
  {:pre [(packet? pk)]}
  (let [id (assert! (numeric-id state (::pk/id pk)))
        encoder (assert! (packet-encoder pk))
        data-buf (-> (Unpooled/buffer)
                     (write-varint! id)
                     (encoder pk))]
    ;; This buffer only contains the data + a varint, which can never be larger than 5 bytes
    (-> (Unpooled/buffer (+ (ByteBuf/.writerIndex data-buf) 5))
        (write-varint! (ByteBuf/.writerIndex data-buf))
        (ByteBuf/.writeBytes data-buf))))