client.clj

Branches > refactor/use-spec-1 > 0e9cbc8 > files / src / net / bowmap / packet / client.clj

(ns net.bowmap.packet.client
  "Clientbound packets"
  (:require
   [clojure.alpha.spec :as s]
   [dev.grahp.build :refer [build]]
   [dev.grahp.error :refer [adorn assert!]]
   [net.bowmap.client.state :as state]
   [net.bowmap.packet :as pk]
   [net.bowmap.packet.write :as write])
  (:import
   [io.netty.buffer ByteBuf CompositeByteBuf Unpooled]))

(defn build-packet-ids [registry]
  (build [[state packet-ids] registry
          [i id] (map-indexed vector packet-ids)]
    [{::state/state state
      ::pk/id id}
     i]))

(defn build-packets [registry]
  (reduce (fn [acc [packet-id table]]
            (assoc acc packet-id (write/writer [:table table])))
          {} registry))

(def ^{:spec (s/map-of (s/schema [::state/state ::pk/id]) ::pk/numeric-id)}
  packet-ids
  "Map of [::state/state ::pk/id] to numeric-id.
   Packet ids (::pk/id) could map directly to numeric-ids and be stateless,
    but some packets have duplicates across connection states that you'd want to handle in the same way.
   If it turns out that I don't end up handling packets in this way, then packet ids could map directly to numeric ids."
  (build-packet-ids
    {::state/status
     [::status-response
      ::pong-response]
     ::state/login
     ;; TODO just because the serialized form is different doesn't mean they have to be different message ids...
     [::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
      ::update-entity-position
      ::update-entity-position-and-rotation
      ::move-minecart-along-track
      ::update-entity-rotation
      ::move-vehicle
      ::open-book
      ::open-screen
      ::open-sign-editor
      ::ping
      ::pong-response
      ::place-ghost-recipe
      ::player-abilities
      ::player-chat-message
      ::end-combat
      ::enter-combat
      ::combat-death
      ::player-info-remove
      ::player-info-update
      ::look-at
      ::synchronize-player-position
      ::player-rotation
      ::recipe-book-add
      ::recipe-book-remove
      ::recipe-book-settings
      ::remove-entities
      ::remove-entity-effect
      ::reset-score
      ::remove-resource-pack
      ::add-resource-pack
      ::respawn
      ::set-head-rotation
      ::update-section-blocks
      ::select-advancements-tab
      ::server-data
      ::set-action-bar-text
      ::set-border-center
      ::set-border-lerp-size
      ::set-border-size
      ::set-border-warning-delay
      ::set-border-warning-distance
      ::set-camera
      ::set-center-chunk
      ::set-render-distance
      ::set-cursor-item
      ::set-default-spawn-position
      ::display-objective
      ::set-entity-metadata
      ::link-entities
      ::set-entity-velocity
      ::set-equipment
      ::set-experience
      ::set-health
      ::set-held-item
      ::update-objectives
      ::set-passengers
      ::set-player-inventory-slot
      ::update-teams
      ::update-score
      ::set-simulation-distance
      ::set-subtitle-text
      ::update-time
      ::set-title-text
      ::set-title-animation-times
      ::entity-sound-effect
      ::sound-effect
      ::start-configuration
      ::stop-sound
      ::store-cookie
      ::system-chat-message
      ::set-tab-list-header-and-footer
      ::tag-query-response
      ::pickup-item
      ::synchronize-vehicle-position
      ::test-instance-block-status
      ::set-ticking-state
      ::step-tick
      ::transfer
      ::update-advancements
      ::update-attributes
      ::entity-effect
      ::update-recipes
      ::update-tags
      ::projectile-power
      ::custom-report-details
      ::server-links
      ::waypoint
      ::clear-dialog
      ::show-dialog-play]}))

(def ^{:spec (s/map-of ::pk/id (s/fspec :args (s/cat :buf #(instance? ByteBuf %) :pk ::pk/packet)
                                        :ret #(instance? ByteBuf %)))}
  packets
  (build-packets
    {::status-response [:response "Response to ::sp/status-request" :json]
     ::pong-response [:timestamp "The same timestamp as ::sp/ping-request" :long]
     ::login-success [:uuid :uuid
                      :username :string
                      :properties [:list [:table [:name :string
                                                  :value :string
                                                  :signature [:optional :string]]]]]
     ::known-packs [:known-packs [:list [:table [:namespace :string
                                                 :id :string
                                                 :version :string]]]]

     ::registry-data [:registry-id :identifier
                      :entries [:list [:table [:id :identifier
                                               :data [:optional :nbt]]]]]
     ::keep-alive [:id :long]
     ::finish-config []
     ::login [:entity-id :int
              :hardcore? :boolean
              :dimension-names [:list :identifier]
              :max-players :varint
              :view-distance :varint
              :simulation-distance :varint
              :reduced-debug-info? :boolean
              :enable-respawn-screen? :boolean
              :do-limited-crafting? :boolean
              :dimension-type-id :varint
              :dimension-name :identifier
              :hashed-seed :long
              ;; TODO :net.bowmap.gamemode ig?
              :gamemode [:enum :ubyte {0 :survival 1 :creative 2 :adventure 3 :spectator}]
              :previous-gamemode [:enum :byte {-1 :unknown 0 :survival 1 :creative 2 :adventure 3 :spectator}]
              :debug-world? :boolean
              :superflat-world? :boolean
              :has-death-location? :boolean
              :death-dimension-name [:context-optional :identifier]
              :death-location [:context-optional :position]
              :portal-cooldown :varint
              :sea-level :varint
              :enforce-secure-chat? :boolean]
     ;; And others too but we don't talk about those
     ::game-event [:event [:enum :byte {3 :set-gamemode 13 :wait-for-chunks}]
                   :value [:or :float 0]]}))

(defn- numeric-id [pk state]
  (assert! (packet-ids {::state/state state ::pk/id (::pk/id pk)})
           {:pk pk :state state}
           "No such packet with that id."))

(defn- packet-encoder [pk]
  (assert! (packets (::pk/id pk))
           {:pk pk}
           "No such encoder for that packet."))

(defn- length-prefixed
  "Returns a composite bytebuf prefixed by the varint length of 'buf'."
  [buf]
  (-> (Unpooled/compositeBuffer 2)
      (CompositeByteBuf/.addComponent true (write/write-varint! (Unpooled/buffer 5) (ByteBuf/.writerIndex buf)))
      (CompositeByteBuf/.addComponent true buf)))

(defn- encoded-packet
  "Returns an encoded packet."
  [pk state]
  (let [encode-body (packet-encoder pk)]
    (length-prefixed (-> (Unpooled/buffer)
                         (write/write-varint! (numeric-id pk state))
                         (encode-body pk)))))

; TODO also update, see sp/decode-packet
; message->packet
(defn encode-packet
  "Encodes packet 'pk' into a ByteBuf of the packet bytes.
   Like decoding, encoding a packet also requires the connection state, as
    packet ids are not unique across connection state, so there isn't a
    direct mapping of packet ids to numeric ids.
   There are several packets (disconnect, keep alive) that are logically the
    same across connection states, and so can be sent without needing to switch
    on the connection state."
  [pk state]
  (adorn (encoded-packet pk state)
         "Error while encoding packet" {:pk pk :state state}))