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