nbt.clj
Branches > main > d4a482d > files / src / net / bowmap / server / packet / nbt.clj
(ns net.bowmap.server.packet.nbt
"The 'Named Binary Tag' format used in packets.
See https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/NBT"
(:require
[clojure.spec.alpha :as s]
[net.bowmap.server.packet.identifier :refer [identifier? tag?]])
(: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?
:tag tag?))
(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))