81fab7e

Bowmap · bowmap.git · Tickets

Branches > main > 81fab7e > files / src / net / bowmap / server

Add NBT writer

Grahp, 2026-10-03

Commit: 81fab7e28e5b48534edfbb5c11f15a8a8e768e1e
Parent: 7593138

7 changed files:

src/net/bowmap/server/protocol/identifier.clj added (view file)

@@ -0,0 +1,67 @@+(ns net.bowmap.server.protocol.identifier+  (:require+   [clojure.string :as str]+   [net.bowmap.server.protocol.schema.minecraft :as mc]))++(def namespace-regex #"^[a-z0-9.\-_]+$")+(def name-regex #"^#?[a-z0-9.\-_:]+$")++(defn identifier?+  "Returns whether 'x' is a valid protocol identifier - a namspace qualified keyword.+   Colons in the name of an identifier may used to separate directories.+   If the identifier's name starts with a number, it must be prefixed with a '#'.+   Identifiers in the 'minecraft' namespace should use the fully qualified minecraft+   schema namespace, rather than :minecraft/name."+  [x]+  (and (qualified-keyword? x)+       (re-matches namespace-regex (namespace x))+       (re-matches name-regex (name x))))++(defn encode-namespace+  "Encodes an identifier namespace into a valid protocol namespace."+  [identifier]+  (if (= (namespace identifier) mc/minecraft-namespace)+    "minecraft"+    (namespace identifier)))++(defn encode-name+  "Encodes an identifier name into a valid protocol name."+  [identifier]+  (-> (name identifier)+      (str/replace-first #"^#" "")+      (str/replace ":" "/")))++(defn encode-identifier+  "Encodes an identifier as a protocol identifier string."+  [identifier]+  (str (encode-namespace identifier) ":" (encode-name identifier)))++(defn decode-namespace+  "Decodes an encoded identifier namespace into a valid identifier object namespace."+  [encoded-namespace]+  (if (= encoded-namespace "minecraft")+    mc/minecraft-namespace+    encoded-namespace))++(defn decode-name+  "Decodes an encoded identifier name into a valid identifier object name."+  [encoded-name]+  (-> encoded-name+      (str/replace #"/" ":")+      (str/replace #"^([0-9])" "#$1")))++(defn decode-identifier+  "Returns an identifier from an encoded string namespace and name.+   Throws if the encoded values are not valid identifiers."+  [namespace name]+  (let [identifier (keyword (decode-namespace namespace) (decode-name name))]+    (if (identifier? identifier)+      identifier+      (throw (ex-info "Cannot decode that value into an identifier."+                      {:namespace namespace :name name})))))++(defn tag+  "Returns the protocol tag of an identifier, which is the encoded identifier+   preceded by a '#'."+  [identifier]+  (apply str "#" (encode-identifier identifier)))

src/net/bowmap/server/protocol/nbt.clj added (view file)

@@ -0,0 +1,111 @@+(ns net.bowmap.server.protocol.nbt+  "Minecraft's 'Named Binary Tag' format.+   See https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/NBT"+  (:require+   [clojure.spec.alpha :as s]+   [net.bowmap.server.protocol.identifier :refer [identifier?]])+  (:refer-clojure :exclude [type]))++(declare type)++(def numeric-rank+  "Map of numeric types to their relative widening order. Higher-ranked types+   can encode lower-ranked types, but with more bits.+   Note that booleans are encoded as the integers 1 and 0 for true and false,+   and are thus numeric types here."+  {::boolean 1 ::byte 2 ::short 3 ::int 4 ::long 5})++(defn list-type+  "Returns the NBT type that can encode *all* values of the given list using+   the fewest number of bytes, or nil if one does not exist. For lists+   containing integers (including booleans, which are represented as the+   integers 1 and 0 for true and false), this means numbers of different+   sizes can be encoded in one list.++   Lists are homogeneous, meaning all values must be encoded with one type.+   Any type could encode an empty list, but ::tag-end (id 0) is returned when+   the list is empty.+   Note that nil is not a valid list, as it would also be a valid compound."+  [xs]+  (when (sequential? xs)+    (reduce+      (fn [acc type]+        (cond (= ::tag-end acc) type+              (= acc type) acc+              (and (numeric-rank acc) (numeric-rank type)) (max-key numeric-rank acc type)+              :else (reduced nil)))+      ::tag-end (map type xs))))++(defn type+  "Returns the NBT type of 'x', or nil if 'x' is not a valid NBT value.+   Nil is assumed to be an empty list, rather than an empty compound."+  [x]+  (cond+    (s/valid? ::string x) ::string+    (boolean? x)          ::boolean+    (float? x)            (if (= x (unchecked-float x)) ::float ::double)+    (s/valid? ::byte x)   ::byte+    (s/valid? ::short x)  ::short+    (s/valid? ::int x)    ::int+    (s/valid? ::long x)   ::long+    (map? x)              ::compound+    :else+    (case (list-type x)+      ::byte ::byte-array+      ::int ::int-array+      ::long ::long-array+      nil nil+      ::list)))++(def nbt-type->id+  "Map of NBT types to their encoded IDs.+   'tag-end' is used to denote the end of a compound, and as the type of+   empty lists.+   Booleans are represented as the integers 1 and 0 for true and false."+  {::tag-end 0+   ::boolean 1+   ::byte 1+   ::short 2+   ::int 3+   ::long 4+   ::float 5+   ::double 6+   ::byte-array 7+   ::string 8+   ::list 9+   ::compound 10+   ::int-array 11+   ::long-array 12})++(defn- int-in? [min x max]+  (and (int? x)+       (<= min x max)))++(s/def ::boolean boolean?)+(s/def ::byte #(int-in? Byte/MIN_VALUE % Byte/MAX_VALUE))+(s/def ::short #(int-in? Short/MIN_VALUE % Short/MAX_VALUE))+(s/def ::int #(int-in? Integer/MIN_VALUE % Integer/MAX_VALUE))+(s/def ::long #(int-in? Long/MIN_VALUE % Long/MAX_VALUE))+(s/def ::float (s/and float? #(= (unchecked-float %) %)))+(s/def ::double float?)+(s/def ::byte-array (s/coll-of ::byte))+(s/def ::string (s/or :string string?+                      :keyword simple-keyword?+                      :identifier identifier?))+(s/def ::list list-type)+(s/def ::compound (s/map-of ::string ::nbt))+(s/def ::int-array (s/coll-of ::int))+(s/def ::long-array (s/coll-of ::long))++(s/def ::nbt (s/or ::boolean ::boolean+                   ::byte ::byte+                   ::short ::short+                   ::int ::int+                   ::long ::long+                   ::double ::double+                   ::byte-array ::byte-array+                   ::string ::string+                   ::list ::list+                   ::compound ::compound+                   ::int-array ::int-array+                   ::long-array ::long-array))

src/net/bowmap/server/protocol/readers.clj modified (view file)

@@ -6,6 +6,7 @@   (:require    [clojure.spec.alpha :as s]    [medley.core :refer [assoc-some]]+   [net.bowmap.server.protocol.identifier :as identifier]    [net.bowmap.server.protocol.types :as types])   (:import    [io.netty.buffer ByteBuf]@@ -90,7 +91,7 @@   [buf]   (let [namespace (string< buf)         name (string< buf)]-    (types/decode-identifier namespace name)))+    (identifier/decode-identifier namespace name)))  (defn uuid< [buf]   (UUID. (long< buf) (long< buf)))

src/net/bowmap/server/protocol/schema.clj modified (view file)

@@ -1,6 +1,6 @@ (ns net.bowmap.server.protocol.schema   (:require    [clojure.spec.alpha :as s]-   [net.bowmap.server.protocol.types :refer [identifier]]))+   [net.bowmap.server.protocol.identifier :refer [identifier?]])) -(s/def ::identifier identifier)+(s/def ::identifier identifier?)

src/net/bowmap/server/protocol/schema/minecraft.clj added (view file)

@@ -0,0 +1,4 @@+(ns net.bowmap.server.protocol.schema.minecraft+  "The default 'minecraft' namespace used in protocol identifiers.")++(def minecraft-namespace (str (ns-name *ns*)))

src/net/bowmap/server/protocol/types.clj modified (view file)

@@ -1,45 +1,3 @@-(ns net.bowmap.server.protocol.types-  (:require-   [clojure.string :as str]))+(ns net.bowmap.server.protocol.types)  (def max-varint-size 5)--(def namespace-regex #"[a-z0-9.\-_]+")-(def name-regex #"[a-z0-9.\-_:]+")--(defn identifier-  "Returns 'x' when it is a valid protocol identifier.-   Identifiers are namespace qualified keywords with the syntax-   ':namespace/foo:bar:baz', where colons separate directories.-   While unqualified identifiers default to the 'minecraft' namespace in-   the protocol, these must be qualifeid as ':minecraft/name'."-  [x]-  (and (qualified-keyword? x)-       (re-matches namespace-regex (namespace x))-       (re-matches name-regex (name x))-       x))--(defn identifier-segments-  "Returns a map of [:namespace :name], the encoded segments of the given identifier.-   Throws if it is not a valid identifier."-  [ident]-  (if (identifier ident)-    {:namespace (namespace ident)-     :name (str/replace (name ident) #":" "/")}-    (throw (ex-info "Cannot encode that value as an identifier."-                    {:value ident}))))--(defn decode-identifier-  "Returns an identifier from an encoded string namespace and name.-   Throws if the encoded values are not valid identifiers."-  [namespace name]-  (or (identifier (keyword namespace (str/replace name #"/" ":")))-      (throw (ex-info "Cannot decode that value into an identifier."-                      {:namespace namespace :name name}))))--(defn encode-identifier-  "Encodes an identifier as a protocol identifier string.-   Throws if it is not valid identifier."-  [ident]-  (let [{:keys! [namespace name]} (identifier-segments ident)]-    (str namespace ":" name)))

src/net/bowmap/server/protocol/writers.clj modified (view file)

@@ -6,8 +6,9 @@    [clojure.data.json :as json]    [clojure.spec.alpha :as s]    [net.bowmap.server.protocol.buffer :as buffer]+   [net.bowmap.server.protocol.identifier :as identifier]    [net.bowmap.server.protocol.schema :as p]-   [net.bowmap.server.protocol.types :as types])+   [net.bowmap.server.protocol.nbt :as nbt])   (:import    [java.nio.charset StandardCharsets]    [java.util UUID]))@@ -127,13 +128,17 @@   ([buf s]    (bytes> buf (String/.getBytes s StandardCharsets/UTF_8)))) +(defn identifier>+  "Writes an identifier as an encoded string."+  [buf identifier]+  (string> buf (identifier/encode-identifier identifier)))+ (defn split-identifier>   "Writes an identifier as 2 strings: the namespace and the name."   [buf identifier]-  (let [{:keys! [namespace name]} (types/identifier-segments identifier)]-    (-> buf-        (string> namespace)-        (string> name))))+  (-> buf+      (string> (identifier/encode-namespace identifier))+      (string> (identifier/encode-name identifier))))  (def json>   "Turns 'x' into JSON and writes it as a string."@@ -182,3 +187,111 @@                          (list> ::p/game-profile-property-name (string> 64)                                 ::p/game-profile-property-value (string> 32767)                                 ::p/game-profile-property-signature (optional> (string> 1024)))))++(defn- utf-16-code-units [s]+  (map int (String/.toCharArray s)))++(defn- write-modified-utf8-byte! [buf code-unit]+  (cond (zero? code-unit) (-> buf+                              (byte> 0xC0)+                              (byte> 0x80))+        (<= 1 code-unit 0x7F) (byte> buf code-unit)+        (<= 0x80 code-unit 0x7FF) (-> buf+                                      (byte> (bit-or 0xC0 (bit-shift-right code-unit 6)))+                                      (byte> (bit-or 0x80 (bit-and code-unit 0x3F))))+        :else (-> buf+                  (byte> (bit-or 0xE0 (bit-shift-right code-unit 12)))+                  (byte> (bit-or 0x80 (bit-and (bit-shift-right code-unit 6) 0x3F)))+                  (byte> (bit-or 0x80 (bit-and code-unit 0x3F))))))++(defn- write-modified-utf8!+  "Writes Java's modified UTF-8 to a buffer.+   See https://docs.oracle.com/javase/8/docs/api/java/io/DataInput.html#modified-utf-8"+  [buf s]+  (reduce write-modified-utf8-byte! buf (utf-16-code-units s)))++(defn nbt-string>+  "Writes an NBT string to buf. May take a string, keyword, or protocol identifier.+   Strings are prefixed by an unsigned short length, followed by up to 65,535+   modified UTF-8 bytes."+  [buf s]+  (let [s (if (identifier/identifier? s)+            (identifier/encode-identifier s)+            (name s))]+    (-> (ushort> buf (count s))+        (write-modified-utf8! s))))++(declare nbt-writer)++(defn nbt-compound>+  "Writes an NBT compound from a map.+   Compounds are written as 0 or more entries of the map, where each entry+   consists of the value's NBT type ID, the NBT string name of the key,+   and the encoded value.+   The encoded compound ends with the 'tag end' byte (0x0A)."+  [buf m]+  (-> (reduce (fn [acc [k v]]+                (let [t (nbt/type v)]+                  (-> acc+                      (byte> (req! nbt/nbt-type->id t))+                      (nbt-string> (name k))+                      ((nbt-writer t) v))))+              buf m)+      (byte> (nbt/nbt-type->id ::nbt/tag-end))))++(defn nbt-list>+  "Writes a homogeneous list of NBT types.+   Lists are prefixed by the ID of their component type"+  [buf xs]+  (let [nbt-type (nbt/list-type xs)+        id (nbt/nbt-type->id nbt-type)+        writer (array>+                 (if (= nbt-type ::nbt/list)+                   (nbt-writer xs)+                   (nbt-writer nbt-type)))]+    (-> buf

Show all 145 lines