client.clj
Branches > main > 7da31a9 > files / src / net / bowmap / network / message / client.clj
; TODO
(ns net.bowmap.network.message.client
"Clientbound packets"
(:require
[clojure.alpha.spec :as s]
[dev.grahp.build :refer [build]]
[dev.grahp.error :refer [adorn assert!]]
[net.bowmap.network.message :as msg]
[net.bowmap.network.packet :as pk]
[net.bowmap.network.packet.types :as t]
[net.bowmap.network.packet.write :refer [boolean-> byte->
context-optional-> enum-> float->
identifier-> int-> json-> list->
long-> nbt-> optional-> or->
position-> string-> table->
ubyte-> uuid-> varint->]]
[net.bowmap.network.state :as state])
(: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/connection-state state
::msg/id id}
i]))
(defn build-packets [registry]
(reduce (fn [acc [packet-id table]]
(assoc acc packet-id (table-> table)))
{} registry))
(def ^{:spec (s/map-of (s/schema [::state/connection-state ::msg/id]) ::pk/id)}
packet-ids
; TODO docstrings shouldn't contain these yeag...
; Update
"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 ::msg/id (s/fspec :args (s/cat :buf #(instance? ByteBuf %) :pk ::msg/message)
: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/connection-state state ::msg/id (::msg/id pk)})
{:pk pk :state state}
"No such packet with that id."))
(defn- packet-encoder [pk]
(assert! (packets (::msg/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)
; TODO document, 5 is the max lenght a varint could be, same with the other magic number 5
(CompositeByteBuf/.addComponent true (varint-> (Unpooled/buffer t/max-varint-size) (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)
(varint-> (numeric-id pk state))
(encode-body pk)))))
(defn message->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 message into packet" {:pk pk :state state}))