client.clj
Branches > main > 169f063 > 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
[::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)))))
(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}))