nbt.clj

Bowmap · bowmap.git · Tickets

Branches > main > bda794d > files / src / net / bowmap / server / protocol / nbt.clj

(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))