diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index 43ccafb..0000000 --- a/backend/.gitignore +++ /dev/null @@ -1,50 +0,0 @@ -# Build results -bin/ -obj/ -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ - -# Visual Studio -.vs/ -.vscode/ -*.sln.iml -*.sln.docstates -*.user -*.userprefs -*.userosscache -*.sln.user - -# ReSharper -.idea/ -*.resharper -*.resharper.user -*.DotSettings.user -_ReSharper*/ -*.[Rr]e[Ss]harper -*.sln.iml - -# Local environment -.env -.env.local -appsettings.Development.json - -# NuGet -.nuget/ -*.nupkg -*.snupkg -packages/ - -# Rider -.idea/ -*.sln.iml -*.log - -# macOS -.DS_Store diff --git a/backend/Backend.csproj b/backend/Backend.csproj deleted file mode 100644 index cc4ddb6..0000000 --- a/backend/Backend.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - Exe - net10.0 - latest - enable - Love2DBackend - Backend - - - - diff --git a/backend/Dockerfile b/backend/Dockerfile index 985edbb..fcf2049 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,14 +1,44 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /build -COPY Backend.csproj . -RUN dotnet restore -COPY . . -RUN dotnet build -c Release +# Build stage +FROM golang:1.25-bookworm AS builder -FROM mcr.microsoft.com/dotnet/runtime:10.0 WORKDIR /app -COPY --from=build /build/bin/Release/net10.0 . -ENV ENET_HOST=0.0.0.0 -ENV ENET_PORT=7777 -EXPOSE 7777/udp -ENTRYPOINT ["./Backend"] + +# 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"] diff --git a/backend/Program.cs b/backend/Program.cs deleted file mode 100644 index 805a683..0000000 --- a/backend/Program.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Love2DBackend -{ - class Program - { - static async Task Main(string[] args) - { - Console.WriteLine("LÖVE2D Backend - UDP Server"); - Console.WriteLine("Starting server on port 7777..."); - - using (var udpServer = new UdpClient(7777)) - { - Console.WriteLine("Server listening on 0.0.0.0:7777"); - - var cts = new CancellationTokenSource(); - - // Handle Ctrl+C gracefully - Console.CancelKeyPress += (s, e) => - { - e.Cancel = true; - cts.Cancel(); - }; - - try - { - while (!cts.Token.IsCancellationRequested) - { - var result = await udpServer.ReceiveAsync(cts.Token); - var message = Encoding.UTF8.GetString(result.Buffer); - var clientEndpoint = result.RemoteEndPoint; - - Console.WriteLine($"[{clientEndpoint}] {message}"); - - // Echo back - var response = Encoding.UTF8.GetBytes($"Echo: {message}"); - await udpServer.SendAsync(response, response.Length, clientEndpoint); - } - } - catch (OperationCanceledException) - { - Console.WriteLine("Server stopped."); - } - } - } - } -} diff --git a/backend/README.md b/backend/README.md index 3bedb6f..07b6749 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,82 +1,30 @@ -# ENET Gaming Backend +# Löve2D ENET Gaming Backend -A multiplayer game server written in C# using ENet for networking. +A multiplayer game server written in GO Lang wrapping ENet for networking. Designed to support mutiple of my LÖVE2D game experiments at once. ## Architecture -``` -┌─────────────────────────────────────────────────────────────┐ -│ LÖVE2D Game Clients │ -│ (Fennel/Lua with Love2D Framework) │ -└────────────────────────────────┬────────────────────────────┘ - │ (UDP via ENet) - │ - ┌────────────┴────────────┐ - │ ENet-CSharp Server │ - │ (Multiplayer Backend) │ - └────────────┬────────────┘ - │ - ┌────────────┴────────────┐ - │ Game State Manager │ - │ Session Storage │ - │ Player Coordination │ - └────────────────────────┘ -``` - ## Technology Stack -- **Language**: C# (.NET) -- **Networking**: [ENet-CSharp](https://github.com/nxrighthere/ENet-CSharp) - reliable UDP multiplayer protocol -- **Deployment**: Docker + Nomad -- **Port**: `7777` (UDP, configurable) +- Language: Go Lang +- Networking: + - https://github.com/codecat/go-enet + - http://enet.bespin.org/index.html +- Docker image +- Nomad deployment -## Quick Start -### Prerequisites +## Local Development -- .NET 8.0 or later -- ENet-CSharp (vendored or via NuGet) -- Docker (for containerization) - -### Local Development - -```bash -cd backend -dotnet build -dotnet run +```shell +$ docker compose up --build ``` -The server will start and listen on `127.0.0.1:7777` by default. +## Prod Deployment -### Testing Connection - -With a LÖVE2D client configured to connect to `localhost:7777`: - -```bash -love ../two_player_cleaning_game +```shell +$ ./build.sh ``` -## Configuration - -Server behavior is controlled via environment variables: - -```bash -ENET_HOST=0.0.0.0 # Bind address -ENET_PORT=7777 # UDP port -``` - -## Resources - -- [Learn X In Y C#](https://learnxinyminutes.com/csharp/) - -## Next Steps - -- [ ] Set up C# project structure -- [ ] Integrate ENet-CSharp dependency -- [ ] Implement basic server startup -- [ ] Define message protocol -- [ ] Create player session management -- [ ] Build Docker image -- [ ] Test Nomad deployment -- [ ] Implement game state synchronization +Then go to homelab repo and deploy the nomad job diff --git a/backend/build.sh b/backend/build.sh new file mode 100755 index 0000000..ee6ad53 --- /dev/null +++ b/backend/build.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +set -e + +export AWS_PROFILE=personal +export AWS_REGION=eu-central-1 + +REPO_NAME="love2d-backend-homelabstack" + +if ! aws ecr describe-repositories --repository-names "$REPO_NAME" >/dev/null 2>&1; then + aws ecr create-repository --repository-name "$REPO_NAME" +fi + +docker buildx build --platform linux/amd64,linux/arm64 -t 853019563312.dkr.ecr.eu-central-1.amazonaws.com/gemini-capsule-homelabstack:latest --push . + +echo "Docker image built and pushed to AWS ECR" diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..205ae29 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,8 @@ +services: + backend: + build: + context: . + dockerfile: Dockerfile + container_name: love2d-backend-dev + ports: + - "8095:8095" diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..c426dd1 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,16 @@ +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 +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..73aced8 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,15 @@ +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= diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..cd7a96d --- /dev/null +++ b/backend/main.go @@ -0,0 +1,76 @@ +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() +}