0880c86
Branches > main > 0880c86 > files
Switch to stateful packe handling
Commit: 0880c86c15c15d4780a26dad09ddcb5d1832a517
Parent: 0bcb212
5 changed files:
src/net/bowmap/core.clj 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 view file
@@ -0,0 +1 @@+(ns net.bowmap.network.client)src/net/bowmap/network/message/client.clj 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 asrc/net/bowmap/network/message/server.clj 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 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))