6d328bc
Branches > main > 6d328bc > files
Keep track of state at the connection level
Commit: 6d328bcea69c7e5a06206e07ce5f8adea40dc1b7
Parent: 359397c
6 changed files:
src/net/bowmap/protocol/client.clj 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 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}]src/net/bowmap/protocol/packet/serverbound.clj 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 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-threadsrc/net/bowmap/server/logic.clj 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]}] (dosyncsrc/net/bowmap/transport/connection.clj 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))))