write.clj

Branches > main > 4768247 > files / src / net / bowmap / packet / write.clj

(ns net.bowmap.packet.write
  "Functions to write Minecraft protocol types.
   See https://minecraft.wiki/w/Java_Edition_protocol/Packets#Data_types"
  (:require
    [clojure.alpha.spec :as s]
    [clojure.data.json :as json]
    [dev.grahp.error :refer [raise]]
    [net.bowmap.packet.types :as t])
  (:import
    [io.netty.buffer ByteBuf]
    [java.nio.charset Charset]
    [java.util UUID]))

(defn write-varint! [buf n]
  (loop [buf buf
         n n]
    (let [b (bit-and n 0x7F)
          nxt (unsigned-bit-shift-right n 7)]
      (if (zero? nxt)
        (ByteBuf/.writeByte buf b)
        (recur (ByteBuf/.writeByte buf (bit-or b 0x80))
               nxt)))))

(defn write-string! [buf s]
  (doto buf
    (write-varint! (count s))
    (ByteBuf/.writeCharSequence s (Charset/forName "UTF-8"))))

(defn write-json! [buf s]
  (write-string! buf (json/write-str s)))

(defn write-array! [buf codec xs]
  (reduce (fn [acc x]
            (codec acc x))
          buf xs))

(defn write-list! [buf codec xs]
  (write-array! (write-varint! buf (count xs)) codec xs))

(defn write-uuid! [buf uuid]
  (-> buf
      (ByteBuf/.writeLong (UUID/.getMostSignificantBits uuid))
      (ByteBuf/.writeLong (UUID/.getLeastSignificantBits uuid))))

;; TODO this is where optionality and nil checking should be, and the packet stuff should use this.
(defn write-table! [buf codec-fields m]
  (reduce (fn [acc {:keys [field codec]}]
            (codec acc (get m field)))
          buf codec-fields))

(declare writers)

;; I think this should have :required/:optional/:prefixed-optional,
;; and have it that non-optional fields are implicity :required and thus nil checked
(defn writer
  "Returns a function of a Bytebuf and a value and encodes/writes 't' to the buf, as defined by 'writers'.
   Throws if no such type exists."
  [t]
  (if (vector? t)
    (let [[t & args] t]
      (apply (writer t) args))
    (or (writers t)
        (raise (str "No such type! " (name t)) {:type t}))))

(defn write-boolean! [buf x]
  (if x
    (ByteBuf/.writeByte buf 1)
    (ByteBuf/.writeByte buf 0)))

(defn write-context-optional! [buf codec x]
  (if x
    (codec buf x)
    buf))

(defn write-optional! [buf codec x]
  (write-boolean! buf x)
  (write-context-optional! buf codec x))

;;; NBT

(defn write-modified-utf8! [buf s]
  (reduce (fn [acc code]
            (cond (zero? code) (-> buf
                                   (ByteBuf/.writeByte 0xC0)
                                   (ByteBuf/.writeByte 0x80))
                  (<= 1 code 0x7F) (ByteBuf/.writeByte acc code)
                  (<= 0x80 code 0x7FF) (-> acc
                                             (ByteBuf/.writeByte (bit-or 0xC0 (bit-shift-right code 6)))
                                             (ByteBuf/.writeByte (bit-or 0x80 (bit-and code 0x3F))))
                  :else (-> acc
                              (ByteBuf/.writeByte (bit-or 0xE0 (bit-shift-right code 12)))
                              (ByteBuf/.writeByte (bit-or 0x80 (bit-and (bit-shift-right code 6) 0x3F)))
                              (ByteBuf/.writeByte (bit-or 0x80 (bit-and code 0x3F))))))
          buf
          (map int (String/.toCharArray s))))

(defn write-nbt-string! [buf s]
  (let [s (name s)]
    (-> buf
        (ByteBuf/.writeShort (count s))
        (write-modified-utf8! s))))

(defn nbt-type [x]
  (cond (string? x) :string
        (ident? x) :string
        (boolean? x) :boolean

        ;; Right now all numbers are encoded as longs since the client allows it, but that's obviously quite wasteful
        (int? x) :long
        (float? x) (if (= (float x) x)
                     :float
                     :double)
        (map? x) :compound
        ;; I believe you can also typed arrays for efficiency as well? I think they might store differently on the client.
        (seqable? x) :list
        :else (raise "Failed to encode NBT value!" {:val x})))

(declare nbt-ids nbt-writers)

(defn write-nbt-list! [buf xs]
  ;; Lists are homogenous, so it just looks at the first element and tries to encode all elements with that type
  ;; Should probably check that all the elements actually have the same nbt type lol
  (let [t (nbt-type (first xs))
        id (nbt-ids t)
        encoder (nbt-writers t)]
    (-> buf
        (ByteBuf/.writeByte id)
        (ByteBuf/.writeInt (count xs))
        (write-array! encoder xs))))

(defn >nbt-compound [buf m]
  (-> (reduce (fn [acc [k v]]
                (let [t (nbt-type v)]
                  (-> acc
                      (ByteBuf/.writeByte (nbt-ids t))
                      (write-nbt-string! (name k))
                      ((nbt-writers t) v))))
              buf m)
      (ByteBuf/.writeByte 0x00)))

(defn nbt-writer
  "Returns the NBT writer function for value 'x'.
   Throws if 'x' cannot be encoded into NBT."
  [x]
  (nbt-writers (nbt-type x)))

(defn write-nbt! [buf x]
  (-> buf
      (ByteBuf/.writeByte 0x0a)
      ((nbt-writer x) x)))

(def nbt-writers {:byte ByteBuf/.writeByte
                  :boolean write-boolean!
                  :short ByteBuf/.writeShort
                  :int ByteBuf/.writeInt
                  :long ByteBuf/.writeLong
                  :float ByteBuf/.writeFloat
                  :double ByteBuf/.writeDouble
                  :string write-nbt-string!
                  :list write-nbt-list!
                  :compound >nbt-compound})

(def nbt-ids {:byte 1
              :boolean 1
              :short 2
              :int 3
              :long 4
              :float 5
              :double 6
              :string 8
              :list 9
              :compound 10})

;; End NBT lol


(def writers
  "Map of type names to functions.
   See https://minecraft.wiki/w/Java_Edition_protocol/Packets#Data_types"
  {:byte   ByteBuf/.writeByte
   :ubyte  ByteBuf/.writeByte
   :short  ByteBuf/.writeShort
   :ushort ByteBuf/.writeShort
   :int    ByteBuf/.writeInt
   :long   ByteBuf/.writeLong
   :varint write-varint!
   :string write-string!
   :json   write-json!
   :uuid   write-uuid!
   :identifier write-string!

   :nbt write-nbt!

   ;; TODO These are kinda named backwards...
   :context-optional (fn [t]
                       (let [codec (writer t)]
                         (fn [buf x]
                           (write-context-optional! buf codec x))))
   :optional (fn [t]
               (let [codec (writer t)]
                 (fn [buf x]
                   (write-optional! buf codec x))))
   :table  (fn [t]
             (let [codec-fields (->> (s/conform ::t/table t)
                                     (mapv #(assoc % :codec (writer (:type %)))))]
               (fn [buf m] (write-table! buf codec-fields m))))
   :list   (fn [t]
             (let [codec (writer t)]
               (fn [buf xs] (write-list! buf codec xs))))})