6d328bc

Branches > main > 6d328bc > files / src / net / bowmap / protocol / packet / serverbound.clj

Keep track of state at the connection level

Grahp, 2026-08-24

Commit: 6d328bcea69c7e5a06206e07ce5f8adea40dc1b7
Parent: 359397c

6 changed files:

src/net/bowmap/protocol/client.clj modified (view file)

@@ -1,14 +1,10 @@ (ns net.bowmap.protocol.client   (:require-   [clojure.core.async :refer [chan]]    [clojure.spec.alpha :as s]-   [net.bowmap.transport.connection :as connection]-   [net.bowmap.protocol.state :as state]))+   [net.bowmap.protocol.state :as state]+   [net.bowmap.transport.connection :as connection]))  (s/def ::client (s/keys :opt [::state/connection-state ::connection/connection]))  (defn client [connection]-  {::state/connection-state ::state/handshaking-   ::connection/connection connection-   ; Not the best place but I can move later-   ::clientbound-packets (chan 1024)})+  {::connection/connection connection})

src/net/bowmap/protocol/handle.clj modified (view file)

@@ -1,7 +1,6 @@ (ns net.bowmap.protocol.handle   (:require    [net.bowmap.protocol.chunk :as chunk]-   [net.bowmap.protocol.client :as client]    [net.bowmap.protocol.entity :as entity]    [net.bowmap.protocol.packet :as pk]    [net.bowmap.protocol.packet.clientbound :as clientbound]@@ -16,22 +15,17 @@    [net.bowmap.server.server :as server]    [net.bowmap.stormbow.event :as event])) +; Could eventually just be a seq of both packets and events if that would be nicer to read and write. (defmulti interpret-packet-  "Interprets one serverbound packet, returns a map of [:client :packets :events :keep-alive?].+  "Interprets one serverbound packet, returns a map of [:packets :events].    Safe to call within a transaction, but may update refs."-  (fn [client packet]+  (fn [packet]     (::pk/id packet))) -(defmethod interpret-packet :default [_ _]+(defmethod interpret-packet :default [_]   {}) -(defmethod interpret-packet ::serverbound/handshaking [client pk]-  (let [{:keys! [intent protocol]} pk]-    {:client (assoc client-                    ::state/connection-state intent-                    :protocol-id protocol)}))--(defmethod interpret-packet ::serverbound/status-request [_ _]+(defmethod interpret-packet ::serverbound/status-request [_]   (let [{::ping/keys! [player-count max-players motd]} (ping/ping-server)]     {:packets [{::pk/id ::clientbound/status-response                 :response {:version {:name "26.2"@@ -41,28 +35,25 @@                            :description motd                            :enforcesSecureChat false}}]})) -(defmethod interpret-packet ::serverbound/ping-request [_ pk]+(defmethod interpret-packet ::serverbound/ping-request [pk]   {:packets [{::pk/id ::clientbound/pong-response               :timestamp (:timestamp pk)}]}) -(defmethod interpret-packet ::serverbound/login-start [client pk]-  (let [{:keys! [uuid username]} pk]-    {:client (assoc client ::player/uuid uuid)-     :packets [{::pk/id ::clientbound/login-success-                :profile {:uuid uuid-                          :username username-                          :properties []}-                :session-id (random-uuid)}]}))+(defmethod interpret-packet ::serverbound/login-start [{:keys! [uuid username]}]+  {:packets [{::pk/id ::clientbound/login-success+              :profile {:uuid uuid+                        :username username+                        :properties []}+              :session-id (random-uuid)}]}) -(defmethod interpret-packet ::serverbound/login-acknowledged [client _]-  {:client (assoc client ::state/connection-state ::state/config)-   :packets [{::pk/id ::clientbound/known-packs+(defmethod interpret-packet ::serverbound/login-acknowledged [_]+  {:packets [{::pk/id ::clientbound/known-packs               ;; TODO also probably good to unbundle this               :known-packs [{:namespace "minecraft"                              :id "core"                              :version "26.2"}]}]}) -(defmethod interpret-packet ::serverbound/known-packs [_ _]+(defmethod interpret-packet ::serverbound/known-packs [_]   {:packets (conj (registry/registry-and-tag-packets)                   {::pk/id ::clientbound/finish-config})}) @@ -90,22 +81,11 @@              :chunk-x x              :chunk-z z)))) -(defmethod interpret-packet ::serverbound/finish-config [client _]-  (let [entity-id (entity/next-entity-id)-        uuid (req! client ::player/uuid)-        client (assoc client-                      ::state/connection-state ::state/play-                      ::entity/id entity-id-                      ::client/playing? true)-        ; TODO should players have entity ids?-        ; Not sure if that's a player-level concern-        ; Players have associated protocol entities, of course, so this may not be needed-        player {::entity/id entity-id-                ::player/uuid uuid}]-    {:client client-     :events [{::event/id ::event/player-joined-               ; This one event passing a player value itself rather than a uuid I don't like...-               ::player/player player}]+(defmethod interpret-packet ::serverbound/finish-config [{:keys! [::player/uuid]}]+  (let [entity-id (entity/next-entity-id)]+    {:events [{::event/id ::event/player-joined+               ::player/uuid uuid+               ::entity/id entity-id}]      :packets (-> [{::pk/id ::clientbound/login                     :entity-id entity-id                     :hardcore? false@@ -142,22 +122,22 @@                          :pitch 0                          :flags #{}}))})) -(defn handle-player-update-movement-packet [{::player/keys! [uuid]}-                                            {:keys [& ::position/x ::position/y ::position/z ::position/yaw ::position/pitch]-                                             ; :flags are currently unused-                                             :select movement}]-  {:events [(merge {::event/id ::event/updated-player-movement-                   ::player/uuid uuid-                   ::position/position movement})]})--(defmethod interpret-packet ::serverbound/set-player-rotation [client pk]-  (handle-player-update-movement-packet client pk))--(defmethod interpret-packet ::serverbound/set-player-position-and-rotation [client pk]-  (handle-player-update-movement-packet client pk))--(defmethod interpret-packet ::serverbound/set-player-position [client pk]-  (handle-player-update-movement-packet client pk))--(defmethod interpret-packet ::serverbound/set-player-movement-flags [client pk]-  (handle-player-update-movement-packet client pk))+;(defn handle-player-update-movement-packet [{::player/keys! [uuid]}+;                                            {:keys [& ::position/x ::position/y ::position/z ::position/yaw ::position/pitch]+;                                             ; :flags are currently unused+;                                             :select movement}]+;  {:events [(merge {::event/id ::event/updated-player-movement+;                   ::player/uuid uuid+;                   ::position/position movement})]})+;+;(defmethod interpret-packet ::serverbound/set-player-rotation [client pk]+;  (handle-player-update-movement-packet client pk))+;+;(defmethod interpret-packet ::serverbound/set-player-position-and-rotation [client pk]+;  (handle-player-update-movement-packet client pk))+;+;(defmethod interpret-packet ::serverbound/set-player-position [client pk]+;  (handle-player-update-movement-packet client pk))+;+;(defmethod interpret-packet ::serverbound/set-player-movement-flags [client pk]+;  (handle-player-update-movement-packet client pk))

src/net/bowmap/protocol/packet/serverbound.clj modified (view file)

@@ -111,6 +111,7 @@   {::handshaking [:protocol ->varint                   :address ->string                   :port ->short+                  ; TODO move to protocol state when that ns is sorted                   :intent (->enum ->varint {1 ::state/status 2 ::state/login 3 ::state/transfer})]     ; Status@@ -257,6 +258,9 @@ (def packet-ids (build-packet-id-index packet-order)) (def packet-decoders (build-packet-decode-functions packets)) +; So _this_ belongs in transport+; As do the above maps and functions+; But the schema is fine here :) (defn decode-packet!   "Decodes a packet from a frame."   [frame connection-state]

src/net/bowmap/protocol/server.clj modified (view file)

@@ -1,12 +1,7 @@ (ns net.bowmap.protocol.server   (:require-   [clojure.core.async :refer [<!! >!! alt!! io-thread put! timeout]]-   [net.bowmap.protocol.client :as client]+   [clojure.core.async :refer [<!! io-thread put!]]    [net.bowmap.protocol.handle :as handle]-   [net.bowmap.protocol.packet :as pk]-   [net.bowmap.protocol.packet.clientbound :as clientbound :refer [encode-packet!]]-   [net.bowmap.protocol.packet.serverbound :refer [decode-packet!]]-   [net.bowmap.protocol.state :as state]    [net.bowmap.server.logic :as logic]    [net.bowmap.server.player :as player]    [net.bowmap.stormbow.effect-execution :refer [execute-effects!]]@@ -14,77 +9,29 @@    [net.bowmap.transport.connection :as connection]    [net.bowmap.transport.server :as mc])) -; Map of uuid -> clientbound packet channel-(defonce client-packets (atom {}))-(defonce keepalive-clients (atom #{}))--(defn send-packet! [uuid packet]-  (put! (req! @client-packets uuid) packet))--(defn adorn-effects-with-client [client events]-  (mapv #(assoc % ::client/client client) events))--(defn handle-serverbound-frame [client frame]-  (let [{:keys! [::state/connection-state ::client/clientbound-packets]} client-        was-playing? (::client/playing? client)-        packet (decode-packet! frame connection-state)-        {:keys [client packets events] :or {:client client}} (handle/interpret-packet client packet)]-    (doseq [pk packets]-      (put! clientbound-packets pk))-    (doseq [event events]-      (execute-effects! (adorn-effects-with-client client (logic/handle-events event))))-    (when (and (not was-playing?) (::client/playing? client))-      (let [{::player/keys! [uuid]} client]-        (swap! client-packets assoc uuid clientbound-packets)-        (swap! keepalive-clients conj uuid)))-    client))--(defn client-lifecycle-  "Loops forever, handling server and client-bound packets.-   Returns the final client value at the end of the client's lifecycle."-  [client]-  (let [{:keys! [::client/clientbound-packets ::connection/connection]} client-        {::connection/keys! [serverbound-frames clientbound-frames]} connection]-    (loop [client client]-      (let [res (try-                  (alt!!-                    serverbound-frames ([frame]-                                        (if (some? frame)-                                          (handle-serverbound-frame client frame)-                                          ::disconnect))-                    clientbound-packets ([packet]-                                         (if (some? packet)-                                           (do (>!! clientbound-frames (encode-packet! packet (::state/connection-state client)))-                                               client)-                                           (throw (Exception. "Clientbound packets was closed idk why")))))-                  (catch Throwable t-                    (Throwable/.printStackTrace t)-                    ::disconnect))]-        (if (= ::disconnect res)-          client-          (recur res))))))+(defn connection-lifecycle [connection]+  (let [{::connection/keys! [serverbound-packets clientbound-packets]} connection]+    (loop []+      (let [{:keys [packets events]} (handle/interpret-packet (<!! serverbound-packets))]+        (doseq [packet packets]+          (put! clientbound-packets packet))+        (doseq [event events]+          (execute-effects! (logic/handle-events event))))+      (recur))))  (defn handle-connection [connection]   (io-thread-    (let [client (client/client connection)-          {::connection/keys! [close-fn]} connection-          client (client-lifecycle client)]-      (when-let [uuid (::player/uuid client)]-        (swap! keepalive-clients disj uuid)-        (execute-effects! (adorn-effects-with-client client-                                                     (logic/handle-events {::event/id ::event/player-left-                                                                           ::player/uuid uuid}))))-      (close-fn))))+    (let [{::connection/keys! [close-fn closed]} connection]+      (io-thread+        (when-let [uuid @closed]+          (execute-effects! (logic/handle-events {::event/id ::event/player-left ::player/uuid uuid}))))+      (try+        (connection-lifecycle connection)+        (finally+          (close-fn))))))  (defn start-server! [port]   (let [connections (mc/start-server! port)]-    (io-thread

Show all 109 lines

src/net/bowmap/server/logic.clj modified (view file)

@@ -11,13 +11,12 @@     (::event/id event)))  ; Want this to be pure, which means you need a way to encode that state change as data?-(defmethod handle-event ::event/player-joined [{::player/keys! [player]}]+(defmethod handle-event ::event/player-joined [{::player/keys! [uuid]}]   (dosync-    (let [{::player/keys! [uuid]} player]-      (if (@server/players uuid)-        (throw (Exception. "Player tried to join with an existing UUID"))-        (commute server/players assoc uuid (ref player)))-      [])))+    (if (@server/players uuid)+      (throw (Exception. "Player tried to join with an existing UUID"))+      (commute server/players assoc uuid {::player/uuid uuid}))+    []))  (defmethod handle-event ::event/player-left [{:keys! [::player/uuid]}]   (dosync

src/net/bowmap/transport/connection.clj modified (view file)

@@ -1,42 +1,87 @@ (ns net.bowmap.transport.connection   "A connection to the Minecraft server."   (:require-   [clojure.core.async :refer [<!! >!! chan io-thread close!]]+   [clojure.core.async :refer [<!! chan close! io-thread put! timeout]]+   [net.bowmap.protocol.packet :as pk]+   [net.bowmap.protocol.packet.clientbound :as clientbound :refer [encode-packet!]]+   [net.bowmap.protocol.packet.serverbound :as serverbound :refer [decode-packet!]]+   [net.bowmap.protocol.state :as state]+   [net.bowmap.server.player :as player]    [net.bowmap.transport.streams :as streams])   (:import    [java.io EOFException]    [java.net Socket SocketException])) +; TODO should all this decoding of packets be here instead?+; Connection state definitely should, so maybe packet stuff should as well+; But packets are protocol level stuff, so idk.+; The packets themselves are protocol-level, their parsing is network/transport level+; Or whatever I want to call this package.+; I think the barrier should be bytebuf readers and writers should be here, and the packets can use those functions+; Since they don't just describe serialization, but also schema and structure+(defn handle-serverbound-frame!+  [state frame]+  ; TODO rename to protocol state, connection state is a misleading name+  (let [{::state/keys! [connection-state]} @state+        {::pk/keys! [id] :as packet} (decode-packet! frame connection-state)]+    (case id+      ::serverbound/handshaking        (swap! state assoc ::state/connection-state (req! packet :intent))+      ::serverbound/login-acknowledged (swap! state assoc ::state/connection-state ::state/config)+      ::serverbound/finish-config      (swap! state assoc ::state/connection-state ::state/play)+      ::serverbound/login-start+      (let [{:keys! [username uuid]} packet]+        (swap! state assoc ::player/username username ::player/uuid uuid))+      nil)+    (let [{:keys [& ::player/uuid] :select player} @state]+      (merge packet player))))+ (defn connection-  "Creates a connection from a java.net.Socket-   Connections can be sent and can receive frames via channels."+  "Creates a connection object from a java.net.Socket+   Connections have a channel of serverbound packets, and a channel of clientbound packets.+   Connections can be closed with ::close-fn, and the promise at ::closed will deliver with+   the connection's known uuid, or nil if the connection does not have one."   [socket]-  (let [serverbound-frames (chan 64)-        clientbound-frames (chan 64)+  (let [serverbound-packets (chan 64)+        clientbound-packets (chan 64)         input-stream (Socket/.getInputStream socket)         output-stream (Socket/.getOutputStream socket)+        state (atom {::state/connection-state ::state/handshaking})+        closed-promise (promise)         conn {::close-fn (fn []                            (Socket/.close socket)-                           (close! serverbound-frames)-                           (close! clientbound-frames))-              ::serverbound-frames serverbound-frames-              ::clientbound-frames clientbound-frames}]+                           (close! serverbound-packets)+                           (close! clientbound-packets))+              ::closed closed-promise+              ::serverbound-packets serverbound-packets+              ::clientbound-packets clientbound-packets}]++    (io-thread+      (loop []+        (when (= ::not-closed (deref (promise) 0 ::not-closed))+          (<!! (timeout 15000))+          (when (#{::state/config ::state/play} (::state/connection-state @state))+            (put! clientbound-packets+                  {::pk/id ::clientbound/keep-alive+                   :id (rand-int Integer/MAX_VALUE)}))+          (recur))))+     (io-thread       (try         (loop []-          (>!! serverbound-frames (streams/read-frame!! input-stream))+          (put! serverbound-packets (handle-serverbound-frame! state (streams/read-frame!! input-stream)))           (recur))         (catch EOFException _)         (catch SocketException _)         (finally-          (close! serverbound-frames)+          (close! serverbound-packets)+          (deliver closed-promise (@state ::player/uuid))           (Socket/.close socket))))     (io-thread       (try         (loop []-          (when-some [frame (<!! clientbound-frames)]-            (streams/write-frame!! frame output-stream)+          (when-some [packet (<!! clientbound-packets)]+            (streams/write-frame!! (encode-packet! packet (req! @state ::state/connection-state)) output-stream)             (recur)))         (finally-          (close! clientbound-frames))))

Show all 102 lines