9879d7f

Bowmap · bowmap.git · Tickets

Branches > main > 9879d7f > files / README.md

Merge branch ticket/server-list-ping

Grahp, 2026-09-16

Commit: 9879d7f0d77dc72ae8b97cd380839cb42b8197ab
Parent: d8f34a3
Parent: 3fafff8

20 changed files:

.gitignore added (view file)

@@ -0,0 +1,4 @@+.clj-kondo+.cpcache+.lsp+.nrepl-port

deps.edn added (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 added (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 added (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 added (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 added (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 added (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 added (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 added (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 added (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 added (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 added (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 added (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 added (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 added (view file)

@@ -0,0 +1 @@+(ns net.bowmap.server.protocol.schema)

src/net/bowmap/server/protocol/serverbound_packets.clj added (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 []

Show all 110 lines

src/net/bowmap/server/protocol/types.clj added (view file)

@@ -0,0 +1,3 @@+(ns net.bowmap.server.protocol.types)++(def max-varint-size 5)

src/net/bowmap/server/protocol/writers.clj added (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 added (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 added (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))))