1a9c01b

Branches > main > 1a9c01b > files / src / net / bowmap

Move client/server handling into net.bowmap.instance

Grahp, 2026-06-02

Commit: 1a9c01b1a15ca418748ac64115b1e12f9d22791e
Parent: e91df30

7 changed files:

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

@@ -1,54 +1,10 @@ (ns net.bowmap.core   (:require-   [clojure.core.async :refer [>!! alt!! chan]]-   [net.bowmap.network.client :as client]-   [net.bowmap.network.message.client :as cl]-   [net.bowmap.network.message.server :as srv]-   [net.bowmap.network.server :as server]-   [net.bowmap.network.state :as state]-   [net.bowmap.packet-handling :as handle]))--;; TODO server should just provide messages in/out--(defn read-and-handle-packet-  "Reads/deserializes one packet into a message, and handles it.-   Because packet reading is dependent on the state of the client, and handling a packet can update the state of the client,-   Reading and handling must be synchronized!"-  [pk client]-  (when-some [msg (srv/packet->message pk (::state/connection-state client))]-    (handle/handle-packet!! client msg)))--; 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-out packets-out]-  (alt!!-    messages-out ([msg]-                  (when msg-                    ; Make one of these keepalive-                    ; How does keepalive begin though?-                    (assert (>!! packets-out (cl/message->packet msg (::state/connection-state client))))-                    client))-    packets-in ([pk]-                (when pk-                  (read-and-handle-packet pk client)))))--; Do I need to close out?-; So clients should be managed by the server and they're not really data are they-(defn client-handler [packets-in packets-out]-  (let [messages-out (chan) ; Use like 1024 so it can block but only when bad things are happening bad things are afoot-        client-state {::state/connection-state ::state/handshaking-                      ; TODO this is used but get rid of it/make it generic and pass it around not a key of player-                      ::client/messages-out messages-out}]-    ; Client is not data-    (loop [client client-state]-      ; TODO this can just... stop the loop-      (when-some [client (handle-packets-and-messages client packets-in messages-out packets-out)]-        (recur client)))))+   [net.bowmap.instance :as instance]+   [net.bowmap.network.server :as server]))  (defn start-server []-  (server/start-server! {:handler #'client-handler-                                 :logger-fn prn-                                 :port 25565}))+  (instance/start! (server/start-server! {:port 25565})))  (defn -main [& _args]   (start-server)

src/net/bowmap/instance.clj added (view file)

@@ -0,0 +1,75 @@+(ns net.bowmap.instance+  (:require+   [clojure.core.async :refer [<!! >!! alt!! chan io-thread put! timeout]]+   [net.bowmap.message-application :as a]+   [net.bowmap.network.client :as client]+   [net.bowmap.network.message :as msg]+   [net.bowmap.network.message.client :as cl]+   [net.bowmap.network.message.server :as srv]+   [net.bowmap.network.state :as state]))++; The logical server, holds a set of clients connected++; Global state D:+(defonce instance (atom {::clients {}+                         ::client-messages-ids->out-channels? {}+                         ::keepalive-client-ids #{}}))++(defn start! [server]+  (swap! instance assoc ::server server)+  server)++(defn client+  "Returns a new client value"+  []+  ; TODO this state should be server-wide, I think.+  {::state/connection-state ::state/handshaking+   ::client/id (random-uuid)})+++(defn send-message! [client msg]+  ; TODO is put! okay?+  (put! ((::client-messages-ids->out-channels? @instance) (::client/id client)) msg))++(defn keep-alive! [client]+  (io-thread+    (loop []+      (<!! (timeout 20000))+      (send-message! client {::msg/id ::cl/keep-alive+                             :id (long (* (rand) Long/MAX_VALUE))})+      (recur))))++(defn read-and-handle-packet+  "Reads/deserializes one packet into a message, and handles it.+   Because packet reading is dependent on the state of the client, and handling a packet can update the state of the client,+   Reading and handling must be synchronized!"+  [pk client]+  (when-some [msg (srv/packet->message pk (::state/connection-state client))]+    (let [{:keys [::client/client ::a/keep-alive? ::a/messages]} (a/apply-message msg client)]+      (when keep-alive?+        (keep-alive! client))+      (doseq [msg messages]+        (send-message! client msg))+      client)))++(defn handle-packets-and-messages [client packets-in messages-out packets-out]+  (alt!!+    messages-out ([msg]+                  (when msg+                    (assert (>!! packets-out (cl/message->packet msg (::state/connection-state client))))+                    client))+    packets-in ([pk]+                (when pk+                  (read-and-handle-packet pk client)))))++(defn connect! [client packets-in packets-out]+  (let [messages-out (chan)]+    (io-thread+      (loop [client client]+        (when-some [client (handle-packets-and-messages client packets-in messages-out packets-out)]+          ; Otherwise, stop?+          (swap! instance update ::clients assoc (::client/id client) client)+          (recur client))))+    (swap! instance update ::clients assoc (::client/id client) client)+    (swap! instance update ::client-messages-ids->out-channels? assoc (::client/id client) messages-out)+    client))

src/net/bowmap/message_application.clj added (view file)

@@ -0,0 +1,104 @@+(ns net.bowmap.message-application+  (:require+   [clojure.alpha.spec :as s]+   [dev.grahp.spec :as S]+   [net.bowmap.network.client :as client]+   [net.bowmap.network.message :as msg]+   [net.bowmap.network.message.client :as cl]+   [net.bowmap.network.message.server :as srv]+   [net.bowmap.network.registry :as registry]+   [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!++(S/def ::messages (s/coll-of ::msg/message :type vector?))+(S/def ::response (s/schema [::client/client ::messages ::keep-alive?])) ; And eventually game events as well :)++; TODO nil?+(defmulti apply-message+  (fn [msg client]+    (::msg/id msg)))+(s/fdef apply-message :args (s/cat ::msg/message ::client/client) :ret ::response)++(defmethod apply-message :default [msg client]+  (prn "Unhandled message:" msg)+  {::client/client client})++(defmethod apply-message ::srv/handshaking [msg client]+  {::client/client (assoc client ::state/connection-state (:intent msg))})++(defmethod apply-message ::srv/status-request [_ client]+  {::client/client client+   ::messages+   [{::msg/id ::cl/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 apply-message ::srv/ping-request [msg client]+  {::client/client client+   ::messages+   [{::msg/id ::cl/pong-response+     :timestamp (:timestamp msg)}]})++(defmethod apply-message ::srv/login-start [msg client]+  {::client/client client+   ::messages+   [{::msg/id ::cl/login-success+     :uuid (:uuid msg)+     :username (:username msg)+     :properties []}]})++(defmethod apply-message ::srv/login-acknowledged [_ client]+  {::client/client (assoc client ::state/connection-state ::state/config)+   ::messages+   [{::msg/id ::cl/known-packs+     ;; TODO also probably good to unbundle this+     :known-packs [{:namespace "minecraft"+                    :id "core"+                    :version "1.12.11"}]}]})++(defmethod apply-message ::srv/known-packs [_ client]+  {::client/client client+   ::messages+   (conj (vec registry/registry-messages)+         {::msg/id ::cl/finish-config})})++;; TODO Keepalive!+(defmethod apply-message ::srv/finish-config [_ client]+  {::client/client (assoc client ::state/connection-state ::state/play)+   ::messages+   [{::msg/id ::cl/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

Show all 105 lines

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

@@ -1 +1,7 @@-(ns net.bowmap.network.client)+(ns net.bowmap.network.client+  (:require+   [clojure.alpha.spec :as s]+   [dev.grahp.spec :as S]))++(S/def ::id uuid?)+(S/def ::client (s/schema [::id]))

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

@@ -295,4 +295,4 @@     on the connection state."   [pk state]   (adorn (encoded-packet pk state)-         "Error while encoding packet" {:pk pk :state state}))+         "Error while encoding message into packet" {:pk pk :state state}))

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

@@ -3,6 +3,7 @@    [aleph.tcp :as tcp]    [clojure.core.async :refer [chan io-thread]]    [manifold.stream :as s]+   [net.bowmap.instance :as instance]    [net.bowmap.network.packet.read :as read :refer [->varint]])   (:import    [io.netty.bootstrap ServerBootstrap]@@ -27,27 +28,19 @@              (recur res'))            res)))))) -(defn logging-handler [handler logger-fn]-  (fn [in out]-    (try-      (handler in out)-      (catch Throwable t-        (logger-fn t)))))--(defn- handle-client [handler logger-fn stream info]+(defn- handle-client [stream info]   (let [packets-in (chan 1 packet-framer-xf)         packets-out (chan)-        handler (logging-handler handler logger-fn)]+        client (instance/client)]     (s/connect stream packets-in)     (s/connect packets-out stream)-    (io-thread (handler packets-in packets-out))))+    (instance/connect! client packets-in packets-out)))  ; Could return a channel of clients? ; Make sure this is just kept an interface, it really only needs to know about packets-in, packets-out, and then maybe how to close/disconnect? (defn start-server!-  "Starts a server at the port, calling 'handler' with an 'in' and 'out' channel of packets."-  [& {:keys [handler port logger-fn]}]-  (tcp/start-server (fn [stream info]-                      (handle-client handler logger-fn stream info))+  ;"Starts a server at the port, calling 'handler' with an 'in' and 'out' channel of packets."+  [& {:keys [port]}]+  (tcp/start-server handle-client                     {:port port                      :bootstrap-transform #(ServerBootstrap/.childOption % ChannelOption/TCP_NODELAY true)}))

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

@@ -1,125 +0,0 @@-(ns net.bowmap.packet-handling-  (:require-   [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.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?-; TODO nil?-(defmulti handle-packet-  (fn [msg client]-    (::msg/id msg)))--; Returns the client and performs side effects...-; Should be run single-threaded?-; Hmm...-; TODO nil?-(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)-  client)--(defmethod handle-packet ::server/handshaking [msg client]-  (assoc client ::state/connection-state (:intent msg)))--(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]-  (send-message! client-                  {::msg/id ::client/pong-response-                   :timestamp (:timestamp msg)})-  client)--(defmethod handle-packet ::server/login-start [msg client]-  (send-message! client-                  {::msg/id ::client/login-success-                   :uuid (:uuid msg)-                   :username (:username msg)-                   :properties []})-  client)--(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/connection-state ::state/config))--(defmethod handle-packet ::server/known-packs [_ client]-  (doseq [msg registry/registry-messages]-    (send-message! client msg))-  (send-message! client-                  {::msg/id ::client/finish-config})-  client)--;; 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/connection-state ::state/play))