0880c86

Branches > namespace-reorganization > 0880c86 > files / LICENSE

Switch to stateful packe handling

Grahp, 2026-05-31

Commit: 0880c86c15c15d4780a26dad09ddcb5d1832a517
Parent: 0bcb212

5 changed files:

src/net/bowmap/core.clj modified (view file)

@@ -2,74 +2,43 @@   (:require    [clojure.core.async :refer [<!! >!! alt!! chan io-thread put! timeout]]    [net.bowmap.network.message :as msg]+   [net.bowmap.network.client :as c]    [net.bowmap.network.state :as state]    [net.bowmap.packet-handling :as handle]    [net.bowmap.network.message.client :as client]    [net.bowmap.network.message.server :as server]    [net.bowmap.server :as server-process])) -;; TODO yeah...--(defn handle-packet-  "Handles one ByteBuf of a framed serialized serverbound packet, and returns a-   map of :client and :packets."-  [client packet-buf]-  (let [pk (server/packet->message packet-buf (::state/state client))]-    (handle/response client pk)))--; could return a map of side-effects as data and be pure.-(defmulti handle-message-  (fn [client msg]-    (:msg/id msg)))--; Should be per-client and able to dispatch to the global thing, probably a tick loop or async dun really matter-(defn handle-message!-  "Handles one serverbound message from 'client', and performs the side effects of that message.-   Returns the new client state."-  [client msg]-  (let [res (handle-message client msg)]-    (or (:client res) client)))--; TODO why is this not qualified?-(defmethod handle-message :server/handshaking [client msg]-  (assoc client ::state/state (:intent msg)))--; Does it make more sense to use a channel here?-(defn handle-messages-  "Handles all the messages in the client, which may involve sending some messages out, and-   sending some async events to the server.-   And also starting a keepalive loop?"-  [client msg])--(defn handle-something-or-other [client in packets-out out]+; TODO bad positional arguments, use a map?+; TODO only send in things to the server, the rest is stateful+(defn handle-packets-and-messages [client packets-in messages-in messages-out packets-out]   (alt!!+    messages-out ([msg]+                  ; Make one of these keepalive+                  ; How does keepalive being though?+                  (assert (>!! packets-out (client/message->packet msg (::state/state client))))+                  client)     ; These could be agents around clients with vectors as well instead of immutable maps holding channels-    in ([buf]-        (when buf-          (let [response (handle-packet client buf)]-            (when (:keepalive response)-              ; TODO this should probably be a global scheduler-              (io-thread-                (loop []-                  (<!! (timeout 5000))-                  (when (>!! packets-out {::msg/id ::client/keep-alive-                                          :id (long (* (rand) Long/MAX_VALUE))})-                    (recur)))))-            (doseq [pk (:response response)]-              (put! packets-out pk))-            (:client response))))-    packets-out ([pk]-                 (assert (>!! out (client/encode-packet pk (::state/state client))))-                 client)))+    ; Put this onto messages-in after translating?+    ; Check messages-out and send to uhh yeah+    ;; TODO do you need to send messages out before reading them?+    packets-in ([buf]+                (when buf+                  (when-some [msg (server/packet->message buf (::state/state client))]+                    (handle/handle-packet!! client msg))))))  ; Do I need to close out?-(defn client-handler [in out]-  (let [packets-out (chan)-        client {::state/state ::state/handshaking}]+(defn client-handler [packets-in packets-out]+  (let [messages-out (chan)+        messages-in (chan)+        client {::state/state ::state/handshaking+                ::c/messages-out messages-out}]+    ; Client is not data     (try (loop [client client]-           (when-some [client (handle-something-or-other client in packets-out out)]+           (when-some [client (handle-packets-and-messages client packets-in messages-in messages-out packets-out)]              (recur client)))          (catch Throwable e+           ; Disconnect/cleanup?            (println "In client handler: " e)))))  (defn start-server []

src/net/bowmap/network/client.clj added (view file)

@@ -0,0 +1 @@+(ns net.bowmap.network.client)

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

@@ -287,7 +287,7 @@  ; TODO also update, see sp/decode-packet ; message->packet-(defn encode-packet+(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

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

@@ -162,7 +162,7 @@                                          :pitch ->float                                          :flags ->byte]})) -; TODO update to reflect packet/message distinction+; TODO is this pure/no external effects besides the buf? (defn packet->message   "Decodes/deserializes one packet (::net.bowmap.packet) from framed ByteBuf 'buf'.    The initial length field of the packet should be stripped from the buffer.

src/net/bowmap/packet_handling.clj modified (view file)

@@ -1,90 +1,123 @@ (ns net.bowmap.packet-handling   (:require-   [net.bowmap.network.state :as state]+   [clojure.core.async :refer [<!! io-thread put! timeout]]+   [net.bowmap.network.client :as c]    [net.bowmap.network.message :as msg]    [net.bowmap.network.message.client :as client]+   [net.bowmap.network.message.server :as server]    [net.bowmap.network.registry :as registry]-   [net.bowmap.network.message.server :as server]))+   [net.bowmap.network.state :as state]))  ; "packet handling" is inherently about the low-level protocol level, so either handle stuff at a low level or turn ; packets into high level game events to be sent somewhere else +; It's per-client, but can send stuff into the player+; And it's stateful!++; Performs a side effect and returns the new client? (defmulti handle-packet   (fn [msg client]     (::msg/id msg))) -; Should return just a client...-; Should be a much better interface as well-; Basically sending a packet into the server?-(defn response-  "Returns a map of a client, and any other keys a packet handler may use.-   Client may be unchanged, or adorned with any information, such as a new state."-  [client msg]-  (let [response (handle-packet msg client)]-    (if (:client response)-      response-      (assoc response :client client))))+; Returns the client and performs side effects...+; Should be run single-threaded?+; Hmm...+(defn handle-packet!! [client msg]+  (handle-packet msg client))++(defn- send-message! [client msg]+  ;; TODO this is bad ofc.+  ;; Clients are kinda intractable?+  ;; TODO uhh can this be async?+  ;; I feel like this should block, no?+  ;; It can't right now because send/rec are in a single alt...+  (put! (::c/messages-out client) msg))  (defmethod handle-packet :default [msg client]-  (prn "Unhandled message:" msg))+  (prn "Unhandled message:" msg)+  client)  (defmethod handle-packet ::server/handshaking [msg client]-  {:client (assoc client ::state/state (:intent msg))})+  (assoc client ::state/state (:intent msg))) -(defmethod handle-packet ::server/status-request [msg client]-  {:response [{::msg/id ::client/status-response-               ;; Should be un-bundled yeah.-               :response {:version {:name "1.21.11"-                                    :protocol 774}-                          :players {:online 0-                                    :max 1000}-                          ; Is this a text component? Could make that interface-                          :description {:text "Bowmap!"}-                          :enforcesSecureChat false}}]})+(defmethod handle-packet ::server/status-request [_ client]+  (send-message! client+                  {::msg/id ::client/status-response+                   ;; Should be un-bundled yeah.+                   :response {:version {:name "1.21.11"+                                        :protocol 774}+                              :players {:online 0+                                        :max 1000}+                              ; Is this a text component? Could make that interface+                              :description {:text "Bowmap!"}+                              :enforcesSecureChat false}})+  client)  (defmethod handle-packet ::server/ping-request [msg client]-  {:response [{::msg/id ::client/pong-response-               :timestamp (:timestamp msg)}]})+  (send-message! client+                  {::msg/id ::client/pong-response+                   :timestamp (:timestamp msg)})+  client)  (defmethod handle-packet ::server/login-start [msg client]-  {:response [{::msg/id ::client/login-success-               :uuid (:uuid msg)-               :username (:username msg)-               :properties []}]})+  (send-message! client+                  {::msg/id ::client/login-success+                   :uuid (:uuid msg)+                   :username (:username msg)+                   :properties []})+  client) -(defmethod handle-packet ::server/login-acknowledged [msg client]-  {:client (assoc client ::state/state ::state/config)-   :response [{::msg/id ::client/known-packs-               ;; TODO also probably good to unbundle this-               :known-packs [{:namespace "minecraft" :id "core" :version "1.12.11"}]}]})+(defmethod handle-packet ::server/login-acknowledged [_ client]+  (send-message! client+                  {::msg/id ::client/known-packs+                   ;; TODO also probably good to unbundle this+                   :known-packs [{:namespace "minecraft"+                                  :id "core"+                                  :version "1.12.11"}]})+  (assoc client ::state/state ::state/config)) -(defmethod handle-packet ::server/known-packs [msg client]-  {:response (conj (vec registry/registry-packets)-                   {::msg/id ::client/finish-config})})+(defmethod handle-packet ::server/known-packs [_ client]+  (doseq [msg registry/registry-packets]+    (send-message! client msg))+  (send-message! client+                  {::msg/id ::client/finish-config})+  client) -(defmethod handle-packet ::server/finish-config [msg client]-  {:client (assoc client ::state/state ::state/play)-   :keepalive true-   :response [{::msg/id ::client/login-              :entity-id 0 ;; TODO magic zero :p-              :hardcore? false-              :dimension-names ["world"]-              :max-players 1000-              :view-distance 32-              :simulation-distance 32-              :reduced-debug-info? false-              :enable-respawn-screen? false-              :do-limited-crafting? false-              :dimension-type-id 0-              :dimension-name "world"-              :hashed-seed 203894-              :gamemode :creative-              :previous-gamemode :unknown-              :debug-world? false-              :superflat-world? true-              :has-death-location? false-              :portal-cooldown 10-              :sea-level 60-              :enforce-secure-chat? true}-              {::msg/id ::client/game-event-               :event :wait-for-chunks}]})+;; TODO Keepalive!+(defmethod handle-packet ::server/finish-config [_ client]+  (send-message! client+                  {::msg/id ::client/login+                   :entity-id 0 ;; TODO magic zero :p+                   :hardcore? false+                   :dimension-names ["world"]+                   :max-players 1000+                   :view-distance 32+                   :simulation-distance 32+                   :reduced-debug-info? false+                   :enable-respawn-screen? false+                   :do-limited-crafting? false+                   :dimension-type-id 0+                   :dimension-name "world"+                   :hashed-seed 203894+                   :gamemode :creative+                   :previous-gamemode :unknown+                   :debug-world? false+                   :superflat-world? true+                   :has-death-location? false+                   :portal-cooldown 10+                   :sea-level 60+                   :enforce-secure-chat? true})++  (send-message! client+                  {::msg/id ::client/game-event+                   :event :wait-for-chunks})++  ; TODO when the client disconnects?+  (io-thread+    (loop []+      (<!! (timeout 5000))+      (send-message! client {::msg/id ::client/keep-alive+                             :id (long (* (rand) Long/MAX_VALUE))})+      (recur)))++  (assoc client ::state/state ::state/play))