1a9c01b
Branches > refactor/use-spec-1 > 1a9c01b > files / LICENSE
Move client/server handling into net.bowmap.instance
Commit: 1a9c01b1a15ca418748ac64115b1e12f9d22791e
Parent: e91df30
7 changed files:
src/net/bowmap/core.clj 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 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 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 60src/net/bowmap/network/client.clj 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 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 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 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))