70c3677
Branches > facts > 70c3677 > files
Add clientbound packets
Commit: 70c3677d9d1008afd37a079e8d6b087c4ccc0f81
Parent: 66269b8
2 changed files:
src/net/bowmap/network/packets/clientbound.clj view file
@@ -0,0 +1,281 @@+(ns net.bowmap.network.packets.clientbound+ (:require+ [net.bowmap.network.packet :as pk]+ [net.bowmap.network.packets.writers :as write+ :refer [bit-mask-> boolean-> byte-> chunk-data-> coerce context-optional->+ double-> enum-> float-> identifier-> int-> json-> length-prefixed+ light-data-> list-> long-> nbt-> optional-> or-> position->+ string-> ubyte-> uuid-> varint->]]+ [net.bowmap.network.registry :as reg]+ [net.bowmap.network.state :as state]+ [net.bowmap.server.gamemode :as gamemode])+ (:import+ [io.netty.buffer Unpooled]))++(def packet-order+ {::state/status+ [::status-response+ ::pong-response]+ ::state/login+ ; TODO do the message ids actually have to be different just because a different encoded format is used for the same logic message?+ [::login-disconnect ; Uses JSON instead of a text component :p+ ::encryption-request+ ::login-success+ ::set-compression+ ::login-plugin-request+ ::cookie-request]+ ::state/config+ [::cookie-request+ ::plugin-message+ ::disconnect+ ::finish-config+ ::keep-alive+ ::ping+ ::reset-chat+ ::registry-data+ ::remove-resource-pack+ ::add-resource-pack+ ::store-cookie+ ::transfer+ ::feature-flags+ ::update-tags+ ::known-packs+ ::custom-report-details+ ::server-links+ ::clear-dialog+ ::show-dialog+ ::code-of-conduct]+ ::state/play+ [::bundle-delimiter+ ::spawn-entity+ ::entity-animation+ ::award-statistics+ ::acknowledge-block-change+ ::set-block-destroy-stage+ ::block-entity-data+ ::block-action+ ::block-update+ ::boss-bar+ ::change-difficulty+ ::chunk-batch-finished+ ::chunk-batch-start+ ::chunk-biomes+ ::clear-titles+ ::command-suggestions-response+ ::commands+ ::close-container+ ::set-container-content+ ::set-container-property+ ::set-container-slot+ ::cookie-request+ ::set-cooldown+ ::chat-suggestions+ ::plugin-message+ ::damage-event+ ::debug-block-value+ ::debug-chunk-value+ ::debug-entity-value+ ::debug-event+ ::debug-sample+ ::delete-message+ ::disconnect+ ::disguised-chat-message+ ::entity-event+ ::teleport-entity+ ::explosion+ ::unload-chunk+ ::game-event+ ::game-test-highlight-position+ ::open-horse-screen+ ::hurt-animation+ ::initialize-world-border+ ::keep-alive+ ::chunk-data-and-update-light+ ::world-event+ ::particle+ ::update-light+ ::login+ ::map-data+ ::merchant-offerssrc/net/bowmap/network/packets/writers.clj view file
@@ -0,0 +1,281 @@+(ns net.bowmap.network.packets.writers+ "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.spec.alpha :as s]+ [clojure.data.json :as json]+ [clojure.set :as set]+ [net.bowmap.network.packets.types :as t])+ (:import+ [io.netty.buffer ByteBuf CompositeByteBuf Unpooled]+ [java.nio.charset Charset]+ [java.util BitSet UUID]))++(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 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]}]+ (codec acc (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)))++; Want to conform the given map to the table, and error if any non-optional keys are missing?+(defn table-> [t]+ (assert (s/valid? ::t/table t))+ (let [table (s/conform ::t/table t)]+ (fn [buf m]+ (write-table! buf table m))))++(defn coerce+ "If writer is an actual writer, returns it. If writer is a vector, turnes it into a table writer."+ [writer]+ (if (vector? writer)+ (table-> writer)+ writer))++; Only coerces these for now idk+(defn array-> [writer]+ (let [writer (coerce 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)))))++;;; 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 (throw (ex-info "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]+ (throw (Exception. "YOU NEVER ACTUALLY IMPLEMENTED THE POSITION TYPE REMEMBER?")))++(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 (t/mapize-vector-or-something m))]+ (fn [buf x]+ (writer buf (m x)))))++(defn bitset-> [buf s]+ (let [bs (BitSet.)]+ (doseq [i s]+ (BitSet/.set bs i true))+ ((list-> long->) buf (BitSet/.toLongArray bs))))++(defn bit-mask-> [writer mask]+ (let [mask (t/mapize-vector-or-something mask)]+ (fn [buf m]+ (let [n (reduce (fn [acc [bit x]]+ (if (contains? m x)+ (bit-set acc bit)+ acc))+ 0 mask)]+ (writer buf n)))))++(defn optional-> [writer]+ (let [writer (context-optional-> writer)]+ (fn [buf x]+ (boolean-> buf x)+ (writer buf x))))++; Chunks++; TODO eventually encode pallates and the different values+; Can probably use the qualified chunk keywords?+(def palatted-container->+ (table-> [:bpe ubyte->+ :palatte varint-> ; Only single values for now :3+ :data (array-> long->)]))++(def chunk-section->+ (table-> [:block-count short->+ :block-states palatted-container->+ :biomes palatted-container->]))++(defn- buf-bytes [buf]+ (let [size (ByteBuf/.writerIndex buf)+ arr (byte-array size)]+ (ByteBuf/.getBytes buf 0 arr)+ arr))++(defn wrapped-bytes->+ "Wraps writer in a writer that will return the list of bytes that writer encodes.+ Used by chunk data."+ [writer]+ (fn [buf x]+ (let [encode-buf (-> (Unpooled/buffer)+ (writer x))]+ ((list-> byte->) buf (buf-bytes encode-buf)))))++(def chunk-data->+ (table-> [:heightmaps (list-> byte->)+ ;; Array should be the size of chunk sections+ ;; TODO array length validation would probably be a good idea+ :data (wrapped-bytes-> (array-> chunk-section->))+ :block-entities (list-> byte->)]))++(def light-data->+ (table-> [:sky-light-mask bitset->+ :block-light-mask bitset->+ :empty-sky-light-mask bitset->+ :empty-block-light-mask bitset->+ :sky-light-arrays (list-> byte->)+ :block-light-arrays (list-> byte->)]))