write.clj

Branches > refactor/use-spec-1 > c4d4bcc > 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-list! [buf codec xs]
  ;; Reduce :p
  (reduce (fn [acc x]
            (codec acc x))
          (write-varint! buf (count xs))
          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))

(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!

   ;; 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))))})