write.clj
Branches > main > 28161d2 > 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))))})