70c3677

Branches > facts > 70c3677 > files

Add clientbound packets

Grahp, 2026-08-18

Commit: 70c3677d9d1008afd37a079e8d6b087c4ccc0f81
Parent: 66269b8

2 changed files:

src/net/bowmap/network/packets/clientbound.clj added (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-offers

Show all 282 lines

src/net/bowmap/network/packets/writers.clj added (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->)]))