write.clj

Branches > main > 67c943e > files / src / net / bowmap / network / packet / write.clj

(ns net.bowmap.network.packet.write
  "Functions to write Minecraft protocol types.
   When given bad data, write functions will throw.
   See https://minecraft.wiki/w/Java_Edition_protocol/Packets#Data_types"
  (:require
   [clojure.alpha.spec :as s]
   [clojure.data.json :as json]
   [clojure.set :as set]
   [dev.grahp.error :refer [adorn raise]]
   [dev.grahp.spec :as S]
   [net.bowmap.network.message :as msg]
   [net.bowmap.network.packet.types :as t])
  (:import
   [io.netty.buffer ByteBuf CompositeByteBuf Unpooled]
   [java.nio.charset Charset]
   [java.util UUID]))

(S/def ::writer (s/fspec :args (s/cat :buf #(instance? ByteBuf %) :msg ::msg/message)
                         :ret #(instance? ByteBuf %)))


(defn 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 length-prefixed
  "Returns a new ByteBuf prefixed by an encoded varint of the length of 'buf'."
  [buf]
  (-> (Unpooled/compositeBuffer 2)
      (CompositeByteBuf/.addComponent true (varint-> (Unpooled/buffer t/max-varint-size) (ByteBuf/.writerIndex buf)))
      (CompositeByteBuf/.addComponent true buf)))


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

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

(defn array-> [writer]
  (fn [buf xs]
    (reduce (fn [acc x]
              (writer acc x))
            buf xs)))

(defn list-> [writer]
  (let [writer (array-> writer)]
    (fn [buf xs]
      (-> (varint-> buf (count xs))
          (writer xs)))))

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

; TODO This is where you could check for optionality I think. If the fields aren't [:optional ...]
; TODO Inline
(defn ^:private write-table! [buf table m]
  (reduce (fn [acc {::t/keys [field codec]}]
            (adorn (codec acc (get m field))
                   "Error while writing message table field" {:field field :writer codec :value (get m field)}))
          buf table))

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

(defn context-optional-> [writer]
  (fn [buf x]
    (if x
      (writer buf x)
      buf)))

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

;; TODO probably get rid of these keywords and just have 2 conds?
(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)
        writer (array-> (nbt-writers t))]
    (-> buf
        (ByteBuf/.writeByte id)
        (ByteBuf/.writeInt (count xs))
        (writer xs))))

(defn write-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 nbt->
  "Writes NBT value 'x' into buf, throws if the value cannot be encoded (this will muck the buffer so don't try to recover)"
  [buf x]
  (-> buf
      (ByteBuf/.writeByte 0x0a)
      ((nbt-writer x) x)))

(def nbt-writers {:byte ByteBuf/.writeByte
                  :boolean 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 write-nbt-compound!})

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

;; End NBT lol

(def byte-> ByteBuf/.writeByte)
(def ubyte-> ByteBuf/.writeByte)
(def short-> ByteBuf/.writeShort)
(def ushort-> ByteBuf/.writeShort)
(def int-> ByteBuf/.writeInt)
(def long-> ByteBuf/.writeLong)
(def float-> ByteBuf/.writeFloat)
(def double-> ByteBuf/.writeDouble)
(def identifier-> string->)
(defn position-> [pos]
  (raise "YOU NEVER ACTUALLY IMPLEMENTED THE POSITION TYPE REMEMBER?"))

; TODO is this right?
(defn or-> [writer default]
  (fn [buf x]
    (let [x (if (nil? x) default x)]
      (writer buf x))))

;; TODO what actually happens when the value is nil?
(defn enum->
  "Writes an enum with writer 't', and a map 'm', which maps encoded values to values, so it is 'backwards'.
   For instance, the map {1 :foo} will write :foo as 1."
  [writer m]
  (let [m (set/map-invert m)]
    (fn [buf x]
      (writer buf (m x)))))

(defn optional-> [writer]
  (let [writer (context-optional-> writer)]
    (fn [buf x]
      (boolean-> buf x)
      (writer buf x))))

; Want to conform the given map to the table, and error if any non-optional keys are missing?
(defn table-> [t]
  (let [table (S/ensure-conform ::t/table t)]
    (fn [buf m]
      (write-table! buf table m))))