client.clj

Branches > refactor/use-spec-1 > 7a196ca > 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 [assert! raise]]
   [net.bowmap.client.state :as state]
   [net.bowmap.packet :as pk]
   [net.bowmap.packet.write :as write])
  (:import
   [io.netty.buffer ByteBuf 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
     [::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 []}))

(defn numeric-id [state id]
  (packet-ids {::state/state state ::pk/id id}))

(defn packet-encoder [pk]
  (packets (::pk/id pk)))

;; Maybe this should just be a function of the packet :p
(defn- encode-packet*
  [pk state]
  {:pre [(pk/packet? pk)]}
  (let [id (assert! (numeric-id state (::pk/id pk)) {:pk pk :state state})
        encoder (assert! (packet-encoder pk))
        data-buf (-> (Unpooled/buffer)
                     (write/write-varint! id)
                     (encoder pk))]
    ;; This buffer only contains the data + a varint, which can never be larger than 5 bytes
    ;; TODO apparently there's a 'CompositeByteBuf' for this very purpose :p
    (-> (Unpooled/buffer (+ (ByteBuf/.writerIndex data-buf) 5))
        (write/write-varint! (ByteBuf/.writerIndex data-buf))
        (ByteBuf/.writeBytes data-buf))))

(defn encode-packet
  "Encodes packet 'pk' into a (new) 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]
  (try (encode-packet* pk state)
       (catch Throwable e
         (raise "Error while encoding packet:" {:pk pk :state state} e))))