write.clj
Branches > client-handling-rewrite > ac6158f > 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]
[clojure.set :as set]
[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]}]
(try
(codec acc (get m field))
(catch Throwable e
(raise "Error while encoding packet field" {:field field :value (get m field)} e))))
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 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 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 write-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
(defn write-enum! [buf codec m x]
(codec buf (m 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
:float ByteBuf/.writeFloat
:double ByteBuf/.writeLong
:varint write-varint!
:string write-string!
:json write-json!
:uuid write-uuid!
:identifier write-string!
:boolean write-boolean!
:nbt write-nbt!
:or (fn [t default]
(fn [buf x]
(let [x (if (nil? x) default x)]
((writer t) buf x))))
;; TODO what actually happens when the value is nil?
:enum (fn [t m]
(let [m (set/map-invert m)]
(fn [buf x]
(write-enum! buf (writer t) m x))))
:position (fn [t pos] (raise "YOU NEVER ACTUALLY IMPLEMENTED THE POSITION TYPE REMEMBER?"))
;; 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))))})