9879d7f
Branches > main > 9879d7f > files / README.md
Merge branch ticket/server-list-ping
Commit: 9879d7f0d77dc72ae8b97cd380839cb42b8197ab
Parent: d8f34a3
Parent: 3fafff8
20 changed files:
.gitignore view file
@@ -0,0 +1,4 @@+.clj-kondo+.cpcache+.lsp+.nrepl-portdeps.edn view file
@@ -0,0 +1,9 @@+{:aliases+ {:run {:exec-fn net.bowmap.server.core/-main+ :extra-deps {nrepl/nrepl {:mvn/version "1.7.0"}}}}+ :deps+ {org.clojure/clojure {:mvn/version "1.13.0-alpha6"}+ dev.weavejester/medley {:mvn/version "1.10.0"}+ org.clojure/core.async {:mvn/version "1.9.865"}+ org.clojure/data.json {:mvn/version "2.5.2"}+ io.netty/netty-buffer {:mvn/version "4.1.111.Final"}}}src/net/bowmap/server/core.clj view file
@@ -0,0 +1,9 @@+(ns net.bowmap.server.core+ (:require+ [net.bowmap.server.network.minecraft-server :as mc]+ [net.bowmap.server.runtime.server :as server]+ [nrepl.server :as nrepl]))++(defn -main [& _args]+ (spit ".nrepl-port" (req! (nrepl/start-server) :port))+ (server/run-server! (mc/server :port 25565)))src/net/bowmap/server/network/connection.clj view file
@@ -0,0 +1,46 @@+(ns net.bowmap.server.network.connection+ "A connection to the Minecraft server."+ (:require+ [clojure.core.async :refer [<!! >!! chan close! io-thread]]+ [clojure.spec.alpha :as s]+ [net.bowmap.server.network.streams :as streams])+ (:import+ [java.io IOException]+ [java.net Socket]))++(s/def ::connection (s/keys :opt [::clientbound-frames ::serverbound-frames]))++(defn connection+ "Returns a connection from a given java.net.Socket.+ Connection has keys [::clientbound-frames ::serverbound-frames], both+ channels of clientbound and serverbound frames respectively.+ The lifecycles of the client socket and both frame channels are coupled."+ [socket]+ (let [clientbound-frames (chan 128)+ serverbound-frames (chan 128)+ input-stream (Socket/.getInputStream socket)+ output-stream (Socket/.getOutputStream socket)]+ (Socket/.setTcpNoDelay socket true)+ (io-thread+ (try+ (loop []+ (when (>!! serverbound-frames (streams/read-frame!! input-stream))+ (recur)))+ (catch IOException _)+ (finally+ (close! serverbound-frames)+ (close! clientbound-frames)+ (Socket/.close socket))))+ (io-thread+ (try+ (loop []+ (when-some [frame (<!! clientbound-frames)]+ (streams/write-frame!! frame output-stream)+ (recur)))+ (catch IOException _)+ (finally+ (close! clientbound-frames)+ (close! serverbound-frames)+ (Socket/.close socket))))+ {::clientbound-frames clientbound-frames+ ::serverbound-frames serverbound-frames}))src/net/bowmap/server/network/minecraft_server.clj view file
@@ -0,0 +1,22 @@+(ns net.bowmap.server.network.minecraft-server+ (:require+ [clojure.core.async :refer [>!! chan close! io-thread]]+ [net.bowmap.server.network.connection :as connection])+ (:import+ [java.net ServerSocket]))++(defn server+ "Starts a Minecraft server at the supplied port and optional bind address.+ Returns a channel of connections that is coupled to the server's lifecycle."+ [& {:keys! [port] :keys [bind-addr backlog] :or {:backlog 50}}]+ (let [server-socket (ServerSocket. port backlog bind-addr)+ connection-chan (chan backlog)]+ (io-thread+ (try+ (loop []+ (when (>!! connection-chan (connection/connection (ServerSocket/.accept server-socket)))+ (recur)))+ (finally+ (close! connection-chan)+ (ServerSocket/.close server-socket))))+ connection-chan))src/net/bowmap/server/network/streams.clj view file
@@ -0,0 +1,70 @@+(ns net.bowmap.server.network.streams+ (:require+ [net.bowmap.server.protocol.frame :refer [frame]]+ [net.bowmap.server.protocol.types :refer [max-varint-size]])+ (:import+ [io.netty.buffer ByteBuf]+ [java.io+ EOFException+ IOException+ InputStream+ OutputStream]))++(defn read-byte!!+ "Blocks and reads an unsigned byte (int) from an InputStream.+ Throws if EOF is reached."+ [input-stream]+ (let [x (InputStream/.read input-stream)]+ (if (= x -1)+ (throw (EOFException. "EOF while reading from InputStream"))+ x)))++(defn read-varint!!+ "Blocks and reads a varint from an InputStream.+ Throws if EOF is reached, or if the encoded varint is invalid."+ [input-stream]+ (loop [num-read 0+ result 0]+ (if (>= num-read max-varint-size)+ (throw (IOException. "VarInt too long"))+ (let [b (read-byte!! input-stream)+ result' (bit-or result (bit-shift-left (bit-and b 0x7F) (* 7 num-read)))]+ (if (zero? (bit-and b 0x80))+ result'+ (recur (inc num-read) result'))))))++(defn read-frame!!+ "Blocks and reads a frame from an InputStream.+ Throws if EOF is reached, or if the encoded frame is invalid."+ [input-stream]+ (let [len (read-varint!! input-stream)+ arr (byte-array len)]+ (loop [offset 0]+ (when (< offset len)+ (let [r (InputStream/.read input-stream arr offset (- len offset))]+ (if (= r -1)+ (throw (EOFException. "EOF while reading frame"))+ (recur (+ offset r))))))+ (frame arr)))++(defn write-varint!!+ "Blocks and writes a varint to an OutputStream."+ [output-stream n]+ (loop [n n]+ (let [b (bit-and n 0x7F)+ nxt (unsigned-bit-shift-right n 7)]+ (if (zero? nxt)+ (OutputStream/.write output-stream b)+ (do (OutputStream/.write output-stream (bit-or b 0x80))+ (recur nxt))))))++(defn write-frame!!+ "Blocks and writes a frame to an OutputStream.+ Throws if the frame cannot be written."+ [frame output-stream]+ (let [len (ByteBuf/.readableBytes frame)+ arr (byte-array len)]+ (ByteBuf/.readBytes frame arr)+ (write-varint!! output-stream len)+ (OutputStream/.write output-stream arr)+ (OutputStream/.flush output-stream)))src/net/bowmap/server/protocol/client.clj view file
@@ -0,0 +1,6 @@+(ns net.bowmap.server.protocol.client+ (:require+ [clojure.spec.alpha :as s]+ [net.bowmap.server.protocol.phase :as phase]))++(s/def ::client (s/keys :opt [::phase/phase ::clientbound-packets]))src/net/bowmap/server/protocol/clientbound_packets.clj view file
@@ -0,0 +1,188 @@+(ns net.bowmap.server.protocol.clientbound-packets+ (:require+ [net.bowmap.server.protocol.phase :as phase]+ [net.bowmap.server.protocol.schema :as p]+ [net.bowmap.server.protocol.writers :refer :all]))++(def clientbound-packets+ "Map of connection phases to pairs of [clientbound-packet-id writer]"+ {::phase/status+ [::status-response status-response>+ ::pong-response [::p/timestamp long>]]+ ::phase/login+ [::disconnect nil+ ::encryption-request nil+ ::login-success nil+ ::set-compression nil+ ::login-plugin-request nil+ ::cookie-request nil]+ ::phase/config+ [::cookie-request nil+ ::plugin-message nil+ ::disconnect nil+ ::finish-config nil+ ::keep-alive nil+ ::ping nil+ ::reset-chat nil+ ::registry-data nil+ ::remove-resource-pack nil+ ::add-resource-pack nil+ ::store-cookie nil+ ::transfer nil+ ::feature-flags nil+ ::update-tags nil+ ::known-packs nil+ ::custom-report-details nil+ ::server-links nil+ ::clear-dialog nil+ ::show-dialog nil+ ::code-of-conduct nil]+ ::phase/play+ [::bundle-delimiter nil+ ::spawn-entity nil+ ::entity-animation nil+ ::award-statistics nil+ ::acknowledge-block-change nil+ ::set-block-destroy-stage nil+ ::block-entity-data nil+ ::block-action nil+ ::block-update nil+ ::boss-bar nil+ ::change-difficulty nil+ ::chunk-batch-finished nil+ ::chunk-batch-start nil+ ::chunk-biomes nil+ ::clear-titles nil+ ::command-suggestions-response nil+ ::commands nil+ ::close-container nil+ ::set-container-content nil+ ::set-container-property nil+ ::set-container-slot nil+ ::cookie-request nil+ ::set-cooldown nil+ ::chat-suggestions nil+ ::plugin-message nil+ ::damage-event nil+ ::debug-block-value nil+ ::debug-chunk-value nil+ ::debug-entity-value nil+ ::debug-event nil+ ::debug-sample nil+ ::delete-message nil+ ::disconnect nil+ ::disguised-chat-message nil+ ::entity-event nil+ ::teleport-entity nil+ ::explosion nil+ ::unload-chunk nil+ ::game-event nil+ ::game-rule-values nil+ ::game-test-highlight-position nil+ ::open-horse-screen nil+ ::hurt-animation nil+ ::initialize-world-border nil+ ::keep-alive nil+ ::chunk-data-and-update-light nil+ ::world-event nil+ ::particle nil+ ::update-light nil+ ::login nil+ ::low-disk-space-warning nil+ ::map-data nil+ ::merchant-offers nil+ ::update-entity-position nil+ ::update-entity-position-and-rotation nil+ ::move-minecart-along-track nil+ ::update-entity-rotation nil+ ::move-vehicle nil+ ::open-book nil+ ::open-screen nil+ ::open-sign-editor nil+ ::ping nil+ ::pong-response nil+ ::place-ghost-recipe nil+ ::player-abilities nil+ ::player-chat-message nil+ ::end-combat nil+ ::enter-combat nil+ ::combat-death nil+ ::player-info-remove nil+ ::player-info-update nil+ ::look-at nil+ ::synchronize-player-position nil+ ::player-rotation nil+ ::recipe-book-add nil+ ::recipe-book-remove nil+ ::recipe-book-settings nil+ ::remove-entities nil+ ::remove-entity-effect nil+ ::reset-score nil+ ::remove-resource-pack nil+ ::add-resource-pack nil+ ::respawn nil+ ::set-head-rotation nil+ ::update-section-blocks nil+ ::select-advancements-tab nil+ ::server-data nil+ ::set-action-bar-text nil+ ::set-border-center nil+ ::set-border-lerp-size nil+ ::set-border-size nil+ ::set-border-warning-delay nil+ ::set-border-warning-distance nil+ ::set-camera nil+ ::set-center-chunk nil+ ::set-render-distance nil+ ::set-cursor-item nil+ ::set-default-spawn-position nil+ ::display-objective nil+ ::set-entity-metadata nil+ ::link-entities nil+ ::set-entity-velocity nil+ ::set-equipment nil+ ::set-experience nil+ ::set-health nil+ ::set-held-item nil+ ::update-objectives nil+ ::set-passengers nil+ ::set-player-inventory-slot nil+ ::update-teams nil+ ::update-score nil+ ::set-simulation-distance nil+ ::set-subtitle-text nil+ ::update-time nil+ ::set-title-text nil+ ::set-title-animation-times nil+ ::entity-sound-effect nil+ ::sound-effect nil+ ::start-configuration nil+ ::stop-sound nil+ ::store-cookie nil+ ::system-chat-message nil+ ::set-tab-list-header-and-footer nil+ ::tag-query-response nil+ ::pickup-item nil+ ::synchronize-vehicle-position nil+ ::test-instance-block-status nil+ ::set-ticking-state nil+ ::step-tick nil+ ::transfer nil+ ::update-advancements nil+ ::update-attributes nil+ ::entity-effect nil+ ::update-recipes nil+ ::update-tags nil+ ::projectile-power nil+ ::custom-report-details nil+ ::server-links nil+ ::waypoint nil+ ::clear-dialog nil+ ::show-dialog-play nil]})++(def phase->id->numeric-id (update-vals clientbound-packets #(zipmap (take-nth 2 %) (range))))+(def phase->id->writer (update-vals clientbound-packets #(-> (apply hash-map %)+ some-vals+ (update-vals ->writer))))++(def ids (set (mapcat #(take-nth 2 %) (vals clientbound-packets))))src/net/bowmap/server/protocol/codec.clj view file
@@ -0,0 +1,33 @@+(ns net.bowmap.server.protocol.codec+ (:require+ [clojure.spec.alpha :as s]+ [net.bowmap.server.protocol.clientbound-packets :as clientbound]+ [net.bowmap.server.protocol.frame :refer [frame]]+ [net.bowmap.server.protocol.packet :as packet]+ [net.bowmap.server.protocol.phase :as phase]+ [net.bowmap.server.protocol.readers :as readers]+ [net.bowmap.server.protocol.serverbound-packets :as serverbound]+ [net.bowmap.server.protocol.writers :as writers]))++(s/def ::codec (s/or :reader ::readers/reader+ :writer ::writers/writer))++(defn encode-packet+ "Encodes a packet into a frame.+ Throws when the packet cannot be encoded."+ [{::phase/keys! [phase]} {::packet/keys! [id] :as packet}]+ (let [numeric-id (req! (clientbound/phase->id->numeric-id phase) id)+ writer (req! (clientbound/phase->id->writer phase) id)]+ (-> (frame)+ (writers/varint> numeric-id)+ (writer packet))))++(defn decode-frame+ "Decodes a packet from a frame.+ Can throw if the frame does not contain a valid packet."+ [{:keys! [::phase/phase]} frame]+ (let [numeric-id (readers/varint< frame)+ id (req! (serverbound/phase->numeric-id->id phase) numeric-id)+ reader (req! (serverbound/phase->id->reader phase) id)]+ (assoc (reader frame)+ ::packet/id id)))src/net/bowmap/server/protocol/component.clj view file
@@ -0,0 +1,16 @@+(ns net.bowmap.server.protocol.component+ "The subset of the protocol text component format used by this server.+ See https://minecraft.wiki/w/Text_component_format for the full format."+ (:require+ [clojure.spec.alpha :as s]))++(s/def ::type #{"text" #_"translatable" #_"score" #_"selector" #_"keybind" #_"nbt"})+(s/def ::text string?)++(s/def ::color #(re-matches #"#[0-9a-fA-F]{6}" %))+(s/def ::extra (s/* ::component))++(s/def ::component (s/keys :opt-un [::type ::text #_::translate #_::score #_::selector #_::keybind #_::nbt+ ::extra+ ::color #_::font #_::bold #_::italic #_::underlined #_::strikethrough #_::obfuscated #_::shadow_color+ #_::insertion #_::click_event #_::hover_event]))src/net/bowmap/server/protocol/frame.clj view file
@@ -0,0 +1,16 @@+(ns net.bowmap.server.protocol.frame+ "A framed byte buffer containing one serialized Minecraft protocol packet.+ A frame contains the packet's numeric id and payload.+ Frames are not length-prefixed.+ See https://minecraft.wiki/w/Java_Edition_protocol/Packets for the format."+ (:require+ [clojure.spec.alpha :as s])+ (:import+ [io.netty.buffer ByteBuf Unpooled]))++(s/def ::frame #(instance? ByteBuf %))++(defn frame+ "Returns an empty frame, or a frame containing the bytes of an array."+ ([] (Unpooled/buffer))+ ([arr] (Unpooled/wrappedBuffer arr)))src/net/bowmap/server/protocol/packet.clj view file
@@ -0,0 +1,12 @@+(ns net.bowmap.server.protocol.packet+ "A Minecraft protocol packet sent either to the server (serverbound),+ or to the client (clientbound).+ See https://minecraft.wiki/w/Java_Edition_protocol/Packets"+ (:require+ [clojure.set :as set]+ [clojure.spec.alpha :as s]+ [net.bowmap.server.protocol.clientbound-packets :as clientbound]+ [net.bowmap.server.protocol.serverbound-packets :as serverbound]))++(s/def ::id (set/union clientbound/ids serverbound/ids))+(s/def ::packet (s/keys :opt [::id]))src/net/bowmap/server/protocol/phase.clj view file
@@ -0,0 +1,5 @@+(ns net.bowmap.server.protocol.phase+ "The phase of connection, also known as a connection state or protocol state in Minecraft."+ (:require [clojure.spec.alpha :as s]))++(s/def ::phase #{::handshaking ::status ::login ::config ::play})src/net/bowmap/server/protocol/readers.clj view file
@@ -0,0 +1,62 @@+(ns net.bowmap.server.protocol.readers+ "Functions for reading values from io.netty.ByteBufs.+ Buffers are assumed to be properly framed and contain exactly 1 packet.+ Functions look like type<, where 'type' is a Minecraft protocol type.+ See https://minecraft.wiki/w/Java_Edition_protocol/Packets#Data_types"+ (:require+ [clojure.spec.alpha :as s]+ [medley.core :refer [assoc-some]]+ [net.bowmap.server.protocol.types :refer [max-varint-size]])+ (:import+ [io.netty.buffer ByteBuf]+ [java.nio.charset Charset]))++(s/def ::reader (s/fspec+ :args (s/cat :frame ::frame)+ :ret any?))++(def byte< ByteBuf/.readByte)+(def ubyte< ByteBuf/.readUnsignedByte)+(def short< ByteBuf/.readShort)+(def ushort< ByteBuf/.readUnsignedShort)+(def int< ByteBuf/.readInt)+(def long< ByteBuf/.readLong)+(def float< ByteBuf/.readFloat)+(def double< ByteBuf/.readDouble)++(defn varint< [buf]+ (loop [num-read 0+ result 0]+ (if (>= num-read max-varint-size)+ (throw (Exception. "VarInt too long"))+ (let [b (ByteBuf/.readByte buf)+ result' (bit-or result (bit-shift-left (bit-and b 0x7F) (* 7 num-read)))]+ (if (zero? (bit-and b 0x80))+ result'+ (recur (inc num-read) result'))))))++(defn string< [buf]+ (let [len (varint< buf)]+ (if (neg? len)+ (throw (Exception. "String length cannot be negative"))+ (ByteBuf/.readCharSequence buf len (Charset/forName "UTF-8")))))++(defn enum< [reader m]+ (fn [buf]+ (m (reader buf))))++(defn table< [table]+ (fn [buf]+ (reduce (fn [acc [field reader]]+ (assoc-some acc field (reader buf)))+ {} (partition 2 table))))++(defn ->reader+ "Attempts to coerces 'x' into a reader. Throws if it cannot.+ Vectors with pairs of [:key reader] are turned into tables readers.+ Nil is coerced to the empty table reader."+ [x]+ (cond+ (or (vector? x) (nil? x)) (table< x)+ (fn? x) x+ :else (throw (IllegalArgumentException. (str "Cannot coerce into reader: " (pr-str x))))))src/net/bowmap/server/protocol/schema.clj view file
@@ -0,0 +1 @@+(ns net.bowmap.server.protocol.schema)src/net/bowmap/server/protocol/serverbound_packets.clj view file
@@ -0,0 +1,109 @@+(ns net.bowmap.server.protocol.serverbound-packets+ (:require+ [net.bowmap.server.protocol.phase :as phase]+ [net.bowmap.server.protocol.readers :refer :all]+ [net.bowmap.server.protocol.schema :as p]))++(def serverbound-packets+ "Map of connection phases to pairs of [serverbound-packet-id reader]"+ {::phase/handshaking+ [::handshaking [:protocol varint<+ :address string<+ :port short<+ ::phase/phase (enum< varint< {1 ::phase/status 2 ::phase/login 3 ::phase/transfer})]]+ ::phase/status+ [::status-request []+ ::ping-request [::p/timestamp long<]]+ ::phase/login+ [::login-start []+ ::encryption-response []+ ::login-plugin-response []+ ::login-acknowledged []+ ::cookie-response []]+ ::phase/config+ [::client-information []+ ::cookie-response []+ ::plugin-message []+ ::finish-config []+ ::keep-alive []+ ::pong []+ ::resource-pack-response []+ ::known-packs []+ ::custom-click-action []+ ::accept-code-of-conduct []]+ ::phase/play+ [::confirm-teleportation []+ ::attack []+ ::query-block-entity-tag []+ ::bundle-item-selected []+ ::change-difficulty []+ ::change-game-mode []+ ::acknowledge-message []+ ::chat-command []+ ::signed-chat-command []+ ::chat-message []+ ::player-session []+ ::chunk-batch-received []+ ::client-status []+ ::client-tick-end []+ ::client-information []+ ::command-suggestions-request []+ ::acknowledge-configuration []+ ::click-container-button []+ ::click-container []+ ::close-container []+ ::change-container-slot-state []+ ::cookie-response []+ ::plugin-message []+ ::debug-subscription-request []+ ::edit-book []+ ::query-entity-tag []+ ::interact []+ ::jigsaw-generate []+ ::keep-alive []+ ::lock-difficulty []+ ::set-player-position []+ ::set-player-position-and-rotation []+ ::set-player-rotation []+ ::set-player-movement-flags []+ ::move-vehicle []+ ::paddle-boat []+ ::pick-item-from-block []+ ::pick-item-from-entity []+ ::ping-request []+ ::place-recipe []+ ::player-abilities []+ ::player-action []+ ::player-command []+ ::player-input []+ ::player-loaded []+ ::pong []+ ::change-recipe-book-settings []+ ::set-seen-recipe []+ ::rename-item []+ ::resource-pack-response []+ ::seen-advancements []+ ::select-trade []+ ::set-beacon-effect []+ ::select-hotbar-slot []+ ::program-command-block []+ ::program-command-block-minecart []+ ::set-creative-mode-slot []+ ::set-game-rules []+ ::program-jigsaw-block []+ ::program-structure-block []+ ::set-test-block []+ ::update-sign []+ ::spectator-action []+ ::swing-arm []+ ::teleport-to-entity []src/net/bowmap/server/protocol/types.clj view file
@@ -0,0 +1,3 @@+(ns net.bowmap.server.protocol.types)++(def max-varint-size 5)src/net/bowmap/server/protocol/writers.clj view file
@@ -0,0 +1,69 @@+(ns net.bowmap.server.protocol.writers+ "Functions for writing values into io.netty.ByteBufs.+ Functions look like type>, where 'type' is a Minecraft protocol type.+ See https://minecraft.wiki/w/Java_Edition_protocol/Packets#Data_types"+ (:require+ [clojure.data.json :as json]+ [clojure.spec.alpha :as s]+ [net.bowmap.server.protocol.schema :as p])+ (:import+ [io.netty.buffer ByteBuf]+ [java.nio.charset Charset]))++(s/def ::writer (s/fspec+ :args (s/cat :frame ::frame :value any?)+ :fn #(= (-> % :args :frame)+ (:ret %))))++(def byte> ByteBuf/.writeByte)+(def ubyte> ByteBuf/.writeByte)+(def short> ByteBuf/.writeShort)+(def ushort> ByteBuf/.writeShort)+(def int> ByteBuf/.writeInt)+(def long> ByteBuf/.writeLong)+(def float> ByteBuf/.writeFloat)+(def double> ByteBuf/.writeDouble)++(defn varint> [buf n]+ (loop [buf buf+ n n]+ (let [b (bit-and n 0x7F)+ nxt (unsigned-bit-shift-right n 7)]+ (if (zero? nxt)+ (ByteBuf/.writeByte buf b)+ (recur (ByteBuf/.writeByte buf (bit-or b 0x80))+ nxt)))))++(defn string> [buf s]+ {:pre [(string? s)]}+ (doto buf+ (varint> (count s))+ (ByteBuf/.writeCharSequence s (Charset/forName "UTF-8"))))++(defn json> [buf s]+ (string> buf (json/write-str s)))++(defn status-response> [buf {::p/keys! [version-name protocol]+ ::p/keys [motd max-player-count online-player-count]}]+ (json> buf+ (cond-> {:version {:name version-name+ :protocol protocol}}+ motd (assoc :description motd)+ (and max-player-count online-player-count) (assoc :players {:max max-player-count+ :online online-player-count}))))++(defn table> [table]+ (fn [buf m]+ (reduce (fn [acc [field writer]]+ (writer acc (get m field)))+ buf (partition 2 table))))++(defn ->writer+ "Attempts to coerces 'x' into a writer. Throws if it cannot.+ Vectors with pairs of [:key writer] are turned into tables writers.+ Nil is not a valid writer."+ [x]+ (cond+ (vector? x) (table> x)+ (fn? x) x+ :else (throw (IllegalArgumentException. (str "Cannot coerce into writer: " (pr-str x))))))src/net/bowmap/server/runtime/packets.clj view file
@@ -0,0 +1,26 @@+(ns net.bowmap.server.runtime.packets+ (:require+ [net.bowmap.server.protocol.clientbound-packets :as clientbound]+ [net.bowmap.server.protocol.component :as component]+ [net.bowmap.server.protocol.packet :as packet]+ [net.bowmap.server.protocol.phase :as phase]+ [net.bowmap.server.protocol.schema :as p]+ [net.bowmap.server.protocol.serverbound-packets :as serverbound]))++(defn apply-packet+ "'Applies' a serverbound packet to a client.+ Returns [client' [packet*]?]"+ [client packet]+ (let [{::packet/keys! [id]} packet]+ (case id+ ::serverbound/handshaking [(assoc client ::phase/phase (req! packet ::phase/phase))]+ ::serverbound/status-request [client [{::packet/id ::clientbound/status-response+ ::p/version-name "26.2"+ ::p/protocol 776+ ::p/motd {::component/text "Bowmap!"+ ::component/color "#9f9f9f"}+ ::p/online-player-count 0+ ::p/max-player-count 1000}]]+ ::serverbound/login-acknowledged [(assoc client ::phase/phase ::phase/config)]+ ::serverbound/finish-config [(assoc client ::phase/phase ::phase/play)]+ [client])))src/net/bowmap/server/runtime/server.clj view file
@@ -0,0 +1,46 @@+(ns net.bowmap.server.runtime.server+ (:require+ [clojure.core.async :refer [<!! alts!! chan io-thread put!]]+ [net.bowmap.server.network.connection :as connection]+ [net.bowmap.server.protocol.client :as client]+ [net.bowmap.server.protocol.codec :as codec]+ [net.bowmap.server.protocol.phase :as phase]+ [net.bowmap.server.runtime.packets :as packets]))++(defn client-step+ ([_] {::phase/phase ::phase/handshaking})+ ([client in x]+ (case in+ ::connection/serverbound-frame+ (let [[client' packets] (packets/apply-packet client (codec/decode-frame client x))]+ [client' {::client/clientbound-packets packets}])++ ::client/clientbound-packet+ [client {::connection/clientbound-frames [(codec/encode-packet client x)]}])))++(defn handle-connection! [{::connection/keys! [serverbound-frames clientbound-frames]}]+ (let [clientbound-packets (chan 1024)]+ (io-thread+ (loop [client-state (client-step nil)]+ (let [[x ch] (alts!! [serverbound-frames clientbound-packets])+ in (condp = ch+ serverbound-frames ::connection/serverbound-frame+ clientbound-packets ::client/clientbound-packet)]+ (when (some? x)+ (let [[client' {frames ::connection/clientbound-frames+ packets ::client/clientbound-packets}]+ (client-step client-state in x)]+ (doseq [frame frames]+ (put! clientbound-frames frame))+ (doseq [packet packets]+ (put! clientbound-packets packet))+ (recur client'))))))))++(defn run-server!+ "Starts running the given server, where server is a channel of connections.+ Blocks until the server closes."+ [server]+ (loop []+ (when-some [connection (<!! server)]+ (handle-connection! connection)+ (recur))))