66269b8

Branches > main > 66269b8 > files / .gitignore

Add packet handling logic

Grahp, 2026-08-18

Commit: 66269b8aaab54f6e8e02777628328c41a42ce6c1
Parent: cbd60bb

11 changed files:

deps.edn modified (view file)

@@ -1,8 +1,9 @@ {:aliases  {:repl {:extra-deps {nrepl/nrepl {:mvn/version "1.7.0"}}-         :main-opts ["-m" "nrepl.cmdline" "--interactive"]}}+         :main-opts ["-m" "nrepl.cmdline" "--interactive"]}+  :run {:exec-fn net.bowmap.server.core/-main}}  :deps  {org.clojure/clojure {:mvn/version "1.13.0-alpha6"}   org.clojure/core.async {:mvn/version "1.9.865"}+  io.netty/netty-buffer {:mvn/version "4.1.111.Final"}   org.clojure/data.json {:mvn/version "2.5.2"}}}-

src/net/bowmap/network/client.clj added (view file)

@@ -0,0 +1,73 @@+(ns net.bowmap.network.client+  (:require+   [net.bowmap.network.handle :refer [handle-packet]]+   [net.bowmap.network.packets.serverbound :refer [decode-packet!]]+   [net.bowmap.network.packets.types :as t]+   [net.bowmap.network.state :as state])+  (:import+   [io.netty.buffer ByteBuf Unpooled]+   [java.io+    EOFException+    IOException+    InputStream+    OutputStream]+   [java.net Socket]))++(defn read-byte!!+  "Blocks and returns an unsigned byte from 'input-stream', or throws if EOF is reached"+  [input-stream]+  (let [x (InputStream/.read input-stream)]+    (if (= -1 x)+      (throw (EOFException. "EOF while reading from InputStream"))+      x)))++(defn read-varint!!+  "Blocks and returns a varint from 'input-stream', or throws if one cannot be read"+  [input-stream]+  (loop [num-read 0+         result 0]+    (if (>= num-read t/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!!+  "Reads a frame from 'input-stream', or throws if one cannot be read"+  [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))))))+    (Unpooled/wrappedBuffer arr)))++(defn write-frame!! [frame output-stream]+  (let [len (ByteBuf/.readableBytes frame)+        arr (byte-array len)]+    (ByteBuf/.readBytes frame arr)+    (OutputStream/.write output-stream arr)+    (OutputStream/.flush output-stream)))++; When do you actually get these frames?+(defn process-serverbound-packet [client frame]+  (let [packet (decode-packet! frame (::state/connection-state client))]+    ; Get some packets back+    ; Send them out :D+    (handle-packet client packet)))++(defn send-clientbound-packet [client]+  ;...+  )++(defn client [socket]+  (agent {::socket socket+          ::state/connection-state ::state/handshaking}+         :error-handler (fn [a ex]+                          (Throwable/.printStackTrace ex *err*)+                          (Socket/.close socket))))

src/net/bowmap/network/handle.clj added (view file)

@@ -0,0 +1,15 @@+(ns net.bowmap.network.handle+  (:require+   [net.bowmap.network.packet :as pk]+   [net.bowmap.network.packets.serverbound :as serverbound]+   [net.bowmap.network.state :as state]))++; Handles incoming packets from clients++(defn handle-packet+  "Handles a severbound packet from a client"+  [client packet]+  (println packet)+  (case (::pk/id packet)+    ::serverbound/handshaking (assoc client ::state/connection-state (req! packet :intent))+    client))

src/net/bowmap/network/packet.clj added (view file)

@@ -0,0 +1 @@+(ns net.bowmap.network.packet)

src/net/bowmap/network/packets/readers.clj added (view file)

@@ -0,0 +1,123 @@+(ns net.bowmap.network.packets.readers+  "Functions for reading data from frames.+   When given bad data, read functions will throw.+   See https://minecraft.wiki/w/Java_Edition_protocol/Packets#Data_types"+  (:require+   [clojure.spec.alpha :as s]+   [net.bowmap.network.packets.types :as t])+  (:import+   [io.netty.buffer ByteBuf]+   [java.nio.charset Charset]+   [java.util UUID]))++(defn ->varint [buf]+  (loop [num-read 0+         result 0]+    (if (>= num-read t/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 ->uuid [buf]+  (UUID. (ByteBuf/.readLong buf) (ByteBuf/.readLong buf)))++#_(defn read-array [codec len buf])++(defn ^:prviate read-bit-mask! [buf reader mask]+  (let [n (reader buf)]+    (reduce-kv (fn [acc bit x]+                 (if (bit-test n bit)+                   (conj acc x)+                   acc))+               #{} mask)))++(def ->byte ByteBuf/.readByte)+(def ->ubyte (comp unchecked-byte ByteBuf/.readByte))+(def ->short ByteBuf/.readShort)+(def ->ushort (comp unchecked-byte ByteBuf/.readShort))+(def ->int ByteBuf/.readInt)+(def ->long ByteBuf/.readLong)+(def ->float ByteBuf/.readFloat)+(def ->double ByteBuf/.readDouble)++(def ->identifer ->string)++(defn ->enum [reader m]+  (let [m (t/mapize-vector-or-something m)]+    (fn [buf]+      (m (reader buf)))))++; Table is a keyword?+(defn ->table [table-fields]+  ; TODO other one too+  (assert (s/valid? ::t/table table-fields))+  (let [table (s/conform ::t/table table-fields)]+    (fn [buf]+      (reduce (fn [acc {::t/keys! [field codec]}]+                (if-some [x (codec buf)]+                         (assoc acc field x)+                         acc))+              {} table))))++(defn coerce+  "If reader is an actual reader, returns it. If reader is a vector, turnes it into a table reader."+  [reader]+  (if (vector? reader)+    (->table reader)+    reader))++; TODO what happens when a bad value is read in+(defn ->bit-mask [reader mask]+  (let [mask (t/mapize-vector-or-something mask)]+    (fn [buf]+      (read-bit-mask! buf reader mask))))++(defn ->position [buf]+  (let [v (ByteBuf/.readLong buf)+        x (bit-shift-right v 38)+        y (bit-shift-right (bit-shift-left v 52) 52)+        z (bit-shift-right (bit-shift-left v 26) 38)]+    {:x x :y y :z z}))++;; TODO untested!+(defn- read-array [reader buf length]+  (loop [i 0+         acc []]+    (if (< i length)+      (recur (inc i)+             (conj acc (reader buf)))+      acc)))+

Show all 124 lines

src/net/bowmap/network/packets/serverbound.clj added (view file)

@@ -0,0 +1,263 @@+(ns net.bowmap.network.packets.serverbound+  (:require+   [net.bowmap.network.packet :as pk]+   [net.bowmap.network.packets.readers+    :refer [->bit-mask ->byte ->double ->enum ->float ->identifier ->int+            ->list ->long ->optional ->position ->short ->string ->ubyte+            ->uuid ->varint coerce]]+   [net.bowmap.network.state :as state]))++(def packet-order+  "Map of connection states to vectors of serverbound packet ids in the order the appear in the protocol."+  {::state/handshaking+   [::handshaking]+   ::state/status+   [::status-request+    ::ping-request]+   ::state/login+   [::login-start+    ::encryption-response+    ::login-plugin-response+    ::login-acknowledged+    ::cookie-response]+   ::state/config+   [::client-information+    ::cookie-response+    ::plugin-message+    ::finish-config+    ::keep-alive+    ::pong+    ::resource-pack-response+    ::known-packs+    ::custom-click-action+    ::accept-code-of-conduct]+   ::state/play+   [::confirm-teleportation+    ::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+    ::set-held-item+    ::program-command-block+    ::program-command-block-minecart+    ::set-creative-mode-slot+    ::program-jigsaw-block+    ::program-structure-block+    ::set-test-block+    ::update-sign+    ::swing-arm+    ::teleport-to-entity+    ::test-instance-block-action+    ::use-item-on+    ::use-item+    ::custom-click-action]})++(def unused-fields [])++(def packets+  "Map of packet ids to schemas"+  ; Handshaking+  {::handshaking [:protocol ->varint+                  :address ->string+                  :port ->short+                  :intent (->enum ->varint {1 ::state/status 2 ::state/login 3 ::state/transfer})]++   ; Status+   ::status-request []+   ::ping-request [:timestamp ->long]++   ; Login+   ::login-start [:username ->string+                  :uuid ->uuid]+   ::encryption-response [:shared-secret (->list ->byte)]+   ::login-plugin-response [:message ->varint+                            :data (fn [buf] (throw (Exception. "Fuck you")))]+   ::login-acknowledged []+   ::cookie-response [:key ->identifier+                      :payload (->optional (->list ->byte))]++   ; Config++   ::client-information unused-fields+   ::plugin-message unused-fields+   ::finish-config []+   ::pong [:id ->int]+   ::resource-pack-response [:uuid ->uuid+                             :result (->enum ->varint {0 :sucessfully-downloaded+                                                       1 :declined+                                                       2 :failed-to-download+                                                       3 :accepted+                                                       4 :downloaded+                                                       5 :invalid-url+                                                       6 :failed-to-reload+                                                       7 :discarded})]+   ; The wiki is too damn confusing+   ::known-packs unused-fields+   #_::custom-click-action+   #_::accept-code-of-conduct++   ;; This packet has more fields I just don't care about them :p+   ;; This packet also has a bunch of fields that I probably do care about!++   ; Play++   ::confirm-teleportation [:teleport-id ->varint]+   #_::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 []+   #_::command-suggestions-request+   #_::acknowledge-configuration+   #_::click-container-button+   #_::click-container+   #_::close-container+   #_::change-container-slot-state+   #_::debug-subscription-request+   #_::edit-book+   #_::query-entity-tag+   #_::interact+   #_::jigsaw-generate+   ::keep-alive [:id ->long]+   #_::lock-difficulty++   ::set-player-rotation [:yaw ->float+                          :pitch ->float+                          :flags ->byte]+   ::set-player-position-and-rotation [:x ->double+                                       :feet-y ->double+                                       :z ->double+                                       :yaw ->float+                                       :pitch ->float+                                       :flags ->byte]+   ::set-player-position [:x ->double+                          :feet-y ->double+                          :z ->double+                          :flags ->byte]++   ::set-player-movement-flags [:flags (->bit-mask ->byte [:on-ground :pushing-against-wall])]+   #_::move-vehicle+   #_::paddle-boat+   #_::pick-item-from-block+   #_::pick-item-from-entity+   ; TODO is this different?+   #_[::ping-request]+   #_::place-recipe+   ::player-abilities [:flags (->bit-mask ->byte {1 :flying?})] ; is this right?+   ::player-action [:status (->enum ->varint [:started-digging :canceled-digging :finished-digging+                                              :drop-item-stack :drop-item :update-item-state :swap-item-in-hand])+                    ; TODO aren't positions and locations different things?+                    :location ->position+                    ; TODO block face might be something logical+                    :face (->enum ->byte [:bottom :top :north :south :west :east])+                    :sequence ->varint]+   ::player-command [:entity-id ->varint+                     :action-id (->enum ->varint [:leave-bed :start-sprinting :stop-sprinting :start-jumping-with-horse+                                                  :stop-jumping-with-horse :open-vehicle-inventory :start-flying-with-elytra])+                     :jump-boost ->varint]+   ::player-input [:flags (->bit-mask ->ubyte [:forward :backward :left :right :jump :sneak :sprint])]+   ::player-loaded []+   #_::change-recipe-book-settings+   #_::set-seen-recipe+   #_::rename-item+   #_::seen-advancements+   #_::select-trade+   #_::set-beacon-effect+   #_::set-held-item+   #_::program-command-block+   #_::program-command-block-minecart+   #_::set-creative-mode-slot+   #_::program-jigsaw-block+   #_::program-structure-block+   #_::set-test-block+   #_::update-sign+   #_::swing-arm+   #_::teleport-to-entity+   #_::test-instance-block-action+   #_::use-item-on+   #_::use-item})++; TODO is this called "registry"?+(defn build-packet-decode-functions [registry]+  (reduce (fn [acc [packet-id table]]+            (assoc acc packet-id+                   (let [reader (coerce table)]+                     (fn [buf]+                       (assoc (reader buf)+                              ::pk/id packet-id)))))+          {} registry))++(defn build-packet-id-index+  "Takes a map of connection states to vectors of packet ids and returns a map of [connection-state numeric-id] to packet ids"+  [registry]+  (into {}+        (for [[state packet-ids] registry+                [i id] (map-indexed vector packet-ids)]+               [{::state/connection-state state+                 ::pk/numeric-id i}+                id])))++(def packet-ids (build-packet-id-index packet-order))+(def packet-decoders (build-packet-decode-functions packets))++(defn decode-packet!+  "Decodes a packet from a frame."+  [frame connection-state]+  (let [numeric-id (->varint frame)+        packet-id (req! packet-ids {::state/connection-state connection-state ::pk/numeric-id numeric-id})+        decode-fn (req! packet-decoders packet-id)]+    (decode-fn frame)))

src/net/bowmap/network/packets/types.clj added (view file)

@@ -0,0 +1,19 @@+(ns net.bowmap.network.packets.types+  (:require+   [clojure.spec.alpha :as s]))++(def max-varint-size 5)+(def max-varlong-size 10) ; right?++(s/def ::table+  (s/* (s/cat ::field keyword?+              ::doc (s/? string?)+              ::codec fn?)))++(defn mapize-vector-or-something+  "Returns a map of keys to vals, where vectors will map their indecies to their values."+  [x]+  {:pre [(or (map? x) (vector? x))]}+  (if (vector? x)+    (zipmap (range) x)+    x))

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

@@ -0,0 +1,26 @@+(ns net.bowmap.network.server+  (:require+   [clojure.core.async :refer [io-thread]]+   [net.bowmap.network.client :as client])+  (:import+   [java.io EOFException]+   [java.net ServerSocket Socket SocketException]))++(defn start-server! [port]+  (let [server (ServerSocket. port)]+    (loop []+      (let [socket (ServerSocket/.accept server)+            input-stream (Socket/.getInputStream socket)+            client (client/client socket)]+        (io-thread+          (try+            (loop []+              ; Close the client connection when EOF is reached+              (let [frame (client/read-frame!! input-stream)]+                (send client client/process-serverbound-packet frame))+              (recur))+            (catch EOFException _)+            (catch SocketException _)+            (finally+              (Socket/.close socket)))))+      (recur))))

src/net/bowmap/network/state.clj added (view file)

@@ -0,0 +1,5 @@++(ns net.bowmap.network.state+  (:require [clojure.spec.alpha :as s]))++(s/def ::connection-state #{::handshaking ::status ::login ::config ::play})

src/net/bowmap/server/core.clj modified (view file)

@@ -1,7 +1,13 @@-(ns net.bowmap.server.core)+(ns net.bowmap.server.core+  (:require+   [net.bowmap.network.server :as server]))  (defn start-server! []-  (println "good day to you world"))+  (Thread/.start (Thread. #(server/start-server! 25565)))+  (println "Server started!"))++(defn -main [& _args]+  (start-server!))  (comment   (start-server!))

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

@@ -0,0 +1,3 @@+(ns net.bowmap.server.game)++; Has a set of connected players