81fab7e
Branches > main > 81fab7e > files / src / net / bowmap / server / game / handle.clj
Add NBT writer
Commit: 81fab7e28e5b48534edfbb5c11f15a8a8e768e1e
Parent: 7593138
7 changed files:
src/net/bowmap/server/protocol/identifier.clj 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 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))+src/net/bowmap/server/protocol/readers.clj 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 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 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 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 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+ (byte> id)+ (int> (count xs))+ (writer xs))))++(defn nbt-typed-array>+ "Returns a writer for a typed NBT array.+ Typed arrays are prefixed by an int length,+ followed by 0 or more values of that type."+ [writer]+ (let [array-writer (array> writer)]+ (fn [buf xs]+ (-> buf+ (int> (count xs))+ (array-writer xs)))))++(defn nbt-writer+ "Returns a writer for the given NBT type.+ Throws if the type is not a valid NBT type."+ [nbt-type]+ (case nbt-type+ ::nbt/tag-end byte>+ ::nbt/boolean boolean>+ ::nbt/byte byte>+ ::nbt/short short>+ ::nbt/int int>+ ::nbt/long long>+ ::nbt/float float>+ ::nbt/double double>+ ::nbt/byte-array (nbt-typed-array> byte>)+ ::nbt/string nbt-string>+ ::nbt/list nbt-list>+ ::nbt/compound nbt-compound>+ ::nbt/int-array (nbt-typed-array> int>)+ ::nbt/long-array (nbt-typed-array> long>)+ (throw (ex-info "Can't encode value into NBT."+ {:nbt-type nbt-type}))))++(defn nbt>+ "Writes an NBT value to buf. Throws if the value is not valid NBT.+ NBT in the protocol is prefixed with the byte 0x0A, followed by the type's payload.+ See https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/NBT#Network_NBT_(Java_Edition)"+ [buf x]+ (-> buf+ (byte> (nbt/nbt-type->id ::nbt/compound))+ ((nbt-writer (nbt/type x)) x)))