switch backend to clojure
This commit is contained in:
parent
99f73315b3
commit
09bf48481a
11 changed files with 228 additions and 159 deletions
1
backend/.cpcache/82615333.basis
Normal file
1
backend/.cpcache/82615333.basis
Normal file
File diff suppressed because one or more lines are too long
1
backend/.cpcache/82615333.cp
Normal file
1
backend/.cpcache/82615333.cp
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,44 +0,0 @@
|
|||
# Build stage
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies including libenet-dev from Debian repos
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
gcc \
|
||||
pkg-config \
|
||||
libenet-dev \
|
||||
sqlite3 \
|
||||
libsqlite3-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy go mod and sum files
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
COPY ./*.go .
|
||||
|
||||
# Enable CGO for go-sqlite3 and go-enet
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
RUN go build -o main .
|
||||
|
||||
# Runtime stage
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libenet7 \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /app/main .
|
||||
|
||||
EXPOSE 8095
|
||||
|
||||
CMD ["./main"]
|
||||
23
backend/deps.edn
Normal file
23
backend/deps.edn
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{:paths ["src"]
|
||||
:deps { ;; db
|
||||
com.datomic/local {:mvn/version "1.0.291"}
|
||||
|
||||
;; logging
|
||||
com.taoensso/telemere {:mvn/version "1.0.0"}
|
||||
|
||||
;; environment variables
|
||||
environ/environ {:mvn/version "1.2.0"}
|
||||
|
||||
;; json / schema validation
|
||||
metosin/malli {:mvn/version "0.18.0"}
|
||||
metosin/muuntaja {:mvn/version "0.6.11"}
|
||||
|
||||
;; networking - UDP/TCP/HTTP streaming
|
||||
aleph/aleph {:mvn/version "0.9.7"}
|
||||
org.clj-commons/byte-streams {:mvn/version "0.3.4"}
|
||||
|
||||
org.clojure/clojure {:mvn/version "1.12.1"}}
|
||||
:aliases
|
||||
{;; Run with clj -T:build function-in-build
|
||||
:build {:deps {io.github.clojure/tools.build {:git/tag "v0.10.9" :git/sha "e405aac"}}
|
||||
:ns-default build}}}
|
||||
10
backend/dev.sh
Executable file
10
backend/dev.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Watch for changes to .clj files and auto-restart the server
|
||||
# fd -e clj: Find all files ending in .clj
|
||||
# entr -r: Run the command when files change, -r flag restarts the command on re-trigger
|
||||
# clojure: Clojure CLI
|
||||
# -M: Use the main alias from deps.edn (if defined)
|
||||
# -m game-server.main: Run the -main function from game-server.main namespace
|
||||
fd -e clj | entr -r clojure -M -m game-server.main
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: love2d-backend-dev
|
||||
ports:
|
||||
- "8095:8095"
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
module love2d_backend
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/codecat/go-libs v0.0.0-20210906174629-ffa6674c8e05
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/codecat/go-enet v0.0.0-20250728072647-ae229138f138
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
github.com/codecat/go-enet v0.0.0-20250728072647-ae229138f138 h1:qQaqly9oVi84i4nLWmM551z7kcFZIy5koCLoPwCarGY=
|
||||
github.com/codecat/go-enet v0.0.0-20250728072647-ae229138f138/go.mod h1:sXkhNvv8T1d/aPEwipUzk7rGWwGJ3uJST2P/Z0zE0L4=
|
||||
github.com/codecat/go-libs v0.0.0-20210906174629-ffa6674c8e05 h1:JSfDXHJvrIpQ8Agy//yoIlGpfIprTCDUytmf68fd/Lc=
|
||||
github.com/codecat/go-libs v0.0.0-20210906174629-ffa6674c8e05/go.mod h1:xJW98cHEb+Kbuu0qmoKzExh3blthZqojIYOFo27VgvE=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
enet "github.com/codecat/go-enet"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := uint16(8095)
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
// Initialize enet
|
||||
slog.Info("Initializing E-Net Server", "port", port)
|
||||
enet.Initialize()
|
||||
|
||||
// Create a host listening on 0.0.0.0:8095
|
||||
host, err := enet.NewHost(enet.NewListenAddress(port), 32, 1, 0, 0)
|
||||
if err != nil {
|
||||
slog.Error("Couldn't create host", "error", err)
|
||||
return
|
||||
}
|
||||
slog.Info("Server started", "port", port)
|
||||
|
||||
// The event loop
|
||||
for true {
|
||||
// Wait until the next event
|
||||
ev := host.Service(1000)
|
||||
|
||||
// Do nothing if we didn't get any event
|
||||
if ev.GetType() == enet.EventNone {
|
||||
continue
|
||||
}
|
||||
|
||||
switch ev.GetType() {
|
||||
case enet.EventConnect: // A new peer has connected
|
||||
slog.Info("New peer connected", "address", ev.GetPeer().GetAddress())
|
||||
|
||||
case enet.EventDisconnect: // A connected peer has disconnected
|
||||
slog.Info("Peer disconnected", "address", ev.GetPeer().GetAddress())
|
||||
|
||||
case enet.EventReceive: // A peer sent us some data
|
||||
// Get the packet
|
||||
packet := ev.GetPacket()
|
||||
|
||||
// We must destroy the packet when we're done with it
|
||||
defer packet.Destroy()
|
||||
|
||||
// Get the bytes in the packet
|
||||
packetBytes := packet.GetData()
|
||||
|
||||
// Respond "pong" to "ping"
|
||||
if string(packetBytes) == "ping" {
|
||||
ev.GetPeer().SendString("pong", ev.GetChannelID(), enet.PacketFlagReliable)
|
||||
continue
|
||||
}
|
||||
|
||||
// Disconnect the peer if they say "bye"
|
||||
if string(packetBytes) == "bye" {
|
||||
slog.Info("Peer sent bye, disconnecting")
|
||||
ev.GetPeer().Disconnect(0)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy the host when we're done with it
|
||||
host.Destroy()
|
||||
|
||||
// Uninitialize enet
|
||||
enet.Deinitialize()
|
||||
}
|
||||
145
backend/src/game_server/main.clj
Normal file
145
backend/src/game_server/main.clj
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
(ns game-server.main
|
||||
(:require
|
||||
[aleph.udp :as udp]
|
||||
[manifold.stream :as s]
|
||||
[clj-commons.byte-streams :as bs]
|
||||
[taoensso.telemere :as t]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[clojure.string :as str])
|
||||
(:import (java.security MessageDigest))
|
||||
(:gen-class))
|
||||
|
||||
;; UDP examples
|
||||
;; - https://gist.github.com/crosstyan/8a87ebdb0c23e549b1c75e9e4013ffa5
|
||||
|
||||
(def port 9999)
|
||||
(def players (atom {})) ; client registry: {player-id {:host ... :port ...}}
|
||||
(def games (atom {:by-id {} :all []}))
|
||||
|
||||
;; Global socket reference for sending
|
||||
(def server-socket (atom nil))
|
||||
|
||||
;; (defn parse-position-msg
|
||||
;; "Parse: 'player-id:x:y' format"
|
||||
;; [msg-str]
|
||||
;; (try
|
||||
;; (let [[player-id x y] (str/split msg-str #":")]
|
||||
;; {:player-id (Integer/parseInt player-id)
|
||||
;; :x (Double/parseDouble x)
|
||||
;; :y (Double/parseDouble y)})
|
||||
;; (catch Exception e
|
||||
;; (t/log! {:level :warn :data {:msg msg-str :error (str e)}} "Failed to parse message")
|
||||
;; nil)))
|
||||
;;
|
||||
|
||||
;; (if waiting-game
|
||||
;; (swap! games (fn [state]
|
||||
;; (let )
|
||||
|
||||
;; :by-id (assoc (:by-id state) player-id {:player-1 }
|
||||
|
||||
;; (-> (:by-id state)
|
||||
;; (assoc player-id waiting-game))
|
||||
;; :waiting (disj (:waiting state) waiting-game)
|
||||
;; :all (conj (:all state) waiting-game)
|
||||
;; })))
|
||||
|
||||
;; (defn get-game-by-player-id [player-id]
|
||||
;; (first (filter (fn [game] (or (= (:player-1 game) player-id) (= (:player-2 game) player-id))) @games)))
|
||||
|
||||
;; (defn get-waiting-game []
|
||||
;; (first (filter (fn [game] (nil? (:player-2 game))) @games)))
|
||||
|
||||
;; (defn join-waiting-game! [player-id other-player-id]
|
||||
;; (if-let [waiting-game (get-game-by-player-id other-player-id)]
|
||||
;; (swap! games update-in [(.indexOf @games waiting-game)] assoc :player-2 player-id)
|
||||
;; (swap! games conj {:player-1 player-id :player-2 nil})))
|
||||
|
||||
(defn md5 [s]
|
||||
(let [md (MessageDigest/getInstance "MD5")
|
||||
digest (.digest md (.getBytes s))]
|
||||
(apply str (map #(format "%02x" %) digest))))
|
||||
|
||||
(defn send-to-player
|
||||
"Send a UDP packet to a player"
|
||||
[player-id message]
|
||||
(if-let [player (get @players player-id)]
|
||||
(try
|
||||
(s/put! @server-socket
|
||||
{:host (:host player)
|
||||
:port (:port player)
|
||||
:message message})
|
||||
(catch Exception e
|
||||
(t/log! {:level :warn :data {:player-id player-id :error (str e)}} "Failed to send to player")))
|
||||
(t/log! {:level :warn :data {:player-id player-id}} "Player not found")))
|
||||
|
||||
(defn join-game! [player-id]
|
||||
(let [waiting-game
|
||||
(some-> (first (filter (fn [game] (nil? (:player-2 game))) (@games :all)))
|
||||
(assoc :player-2 player-id))
|
||||
other-player-id (:player-1 waiting-game)
|
||||
new-game {:player-1 player-id :player-2 nil}]
|
||||
(if waiting-game
|
||||
(do
|
||||
(swap! games (fn [state]
|
||||
{:by-id
|
||||
(-> (:by-id state)
|
||||
(assoc player-id waiting-game)
|
||||
(assoc other-player-id waiting-game))
|
||||
:all (map (fn [game] (if (= (:player-1 game) other-player-id)
|
||||
waiting-game
|
||||
game)) (:all state))}))
|
||||
(send-to-player other-player-id "READY_TO_PLAY")
|
||||
(send-to-player player-id "READY_TO_PLAY"))
|
||||
(do
|
||||
(swap! games (fn [state]
|
||||
{:by-id (assoc (:by-id state) player-id new-game)
|
||||
:all (conj (:all state) new-game)}))
|
||||
(send-to-player player-id "WAIT")))))
|
||||
|
||||
(defn register-player! [host port]
|
||||
(let [player-id (md5 (str host port))]
|
||||
(swap! players assoc player-id {:host host :port port})
|
||||
(t/log! {:level :info :data {:player-id player-id}} "Player registered")
|
||||
(join-game! player-id)
|
||||
(pprint {:games @games :players @players})))
|
||||
|
||||
;; (defn broadcast-to-others [from-player-id position-data]
|
||||
;; "Send position to all other players"
|
||||
;; (let [other-players (dissoc @players from-player-id)
|
||||
;; msg (str from-player-id ":" (:x position-data) ":" (:y position-data))]
|
||||
;; (doseq [[pid _] other-players]
|
||||
;; (send-to-player pid msg))))
|
||||
|
||||
(defn parse-packet [packet]
|
||||
(let [host (-> packet (:sender) (bean) (:address) (bean) (:hostAddress))
|
||||
port (-> packet (:sender) (bean) (:port))
|
||||
vector-msg (-> packet (:message) (vec))
|
||||
string-msg (bs/to-string (:message packet))
|
||||
msg-parts (str/split string-msg #"#")]
|
||||
{:host host
|
||||
:port port
|
||||
:msg (:message packet)
|
||||
:msg-vec vector-msg
|
||||
:msg-string string-msg
|
||||
:msg-parts msg-parts}))
|
||||
|
||||
(defn packet-consumer [packet]
|
||||
(t/log! {:level :info :data {:packet packet}} "Consuming packet")
|
||||
(case (first (:msg-parts packet))
|
||||
"REGISTER" (register-player! (:host packet) (:port packet))))
|
||||
|
||||
(defn start-server [port]
|
||||
(let [socket @(udp/socket {:port port})]
|
||||
(reset! server-socket socket)
|
||||
(t/log! {:level :info :data {:port port}} "UDP server listening")
|
||||
|
||||
(->> socket
|
||||
(s/map parse-packet)
|
||||
(s/consume packet-consumer))
|
||||
socket))
|
||||
|
||||
(defn -main []
|
||||
(t/log! {:level :info :data {:port port}} "Game server starting")
|
||||
(start-server port)
|
||||
@(promise)) ;; keep server running
|
||||
48
backend/test-server.sh
Executable file
48
backend/test-server.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Simple UDP test script for the game server
|
||||
# Sends registration and position updates to simulate two players
|
||||
|
||||
HOST="localhost"
|
||||
PORT=9999
|
||||
|
||||
echo "Testing game server at $HOST:$PORT"
|
||||
echo ""
|
||||
|
||||
# Helper to send UDP packet
|
||||
send_udp() {
|
||||
local msg="$1"
|
||||
echo "Sending: $msg"
|
||||
echo -n "$msg" | nc -u -w1 "$HOST" "$PORT"
|
||||
}
|
||||
|
||||
echo "=== Registering Player 1 ==="
|
||||
send_udp "REGISTER"
|
||||
nc -u -l 0.0.0.0 12345
|
||||
# sleep 0.5
|
||||
|
||||
# echo ""
|
||||
# echo "=== Registering Player 2 ==="
|
||||
# send_udp "register:2"
|
||||
# sleep 0.5
|
||||
|
||||
# echo ""
|
||||
# echo "=== Player 1 moving around ==="
|
||||
# for i in {1..3}; do
|
||||
# x=$((100 + i * 10))
|
||||
# y=$((200 + i * 5))
|
||||
# send_udp "1:$x:$y"
|
||||
# sleep 0.3
|
||||
# done
|
||||
|
||||
# echo ""
|
||||
# echo "=== Player 2 moving around ==="
|
||||
# for i in {1..3}; do
|
||||
# x=$((300 - i * 10))
|
||||
# y=$((150 + i * 20))
|
||||
# send_udp "2:$x:$y"
|
||||
# sleep 0.3
|
||||
# done
|
||||
|
||||
echo ""
|
||||
echo "Done!"
|
||||
Loading…
Add table
Add a link
Reference in a new issue