diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..82a766b --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,17 @@ +# ---- Gateway Dockerfile ---- +# Uses Bun to run TypeScript directly (no build step needed) +FROM oven/bun:1-alpine AS base + +WORKDIR /app + +# Install dependencies +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile --production + +# Copy source +COPY . . + +ENV PORT=3000 +EXPOSE 3000 + +CMD ["bun", "run", "index.ts"] diff --git a/gateway/helper/generateRoom.ts b/gateway/helper/generateRoom.ts new file mode 100644 index 0000000..5938677 --- /dev/null +++ b/gateway/helper/generateRoom.ts @@ -0,0 +1,9 @@ +const generateRoom = () => { + const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + let code = ""; + for (let i = 0; i < 5; i++) + code += chars[Math.floor(Math.random() * chars.length)]; + return code; +}; + +export default generateRoom; diff --git a/gateway/index.ts b/gateway/index.ts index 272e303..8fabec5 100644 --- a/gateway/index.ts +++ b/gateway/index.ts @@ -1,24 +1,21 @@ import createRoom from "./routes/createRoom"; import express from "express"; import addToRoom from "./routes/addToRoom"; -import { createClient } from "redis"; -const redisURL: string = process.env.REDIS_URL!; -const client = createClient({ url: redisURL }); -try { - await client.connect(); -} catch (e) { - console.error(e); - process.exit(1); -} const app = express(); +app.use(express.json()); const port = process.env.PORT || 3000; +let roomMaps: { [id: string]: { [id: string]: string | number } } = {}; -app.post("/add", addToRoom); +// Kubernetes health probe +app.get("/healthz", (_, res) => res.status(200).send("ok")); +// Adds a client to a given room +app.get("/add", addToRoom); +// Allocates a room to a client app.get("/createRoom", createRoom); app.listen(port, () => { console.log(`Listening on ${port}`); }); -export default client; +export default roomMaps; diff --git a/gateway/routes/addToRoom.ts b/gateway/routes/addToRoom.ts index 6b7b161..9c32ffa 100644 --- a/gateway/routes/addToRoom.ts +++ b/gateway/routes/addToRoom.ts @@ -1,17 +1,20 @@ import { type Request, type Response } from "express"; -import client from "../index.ts"; +import roomMaps from "../index.ts"; const addToRoom = async (req: Request, res: Response) => { - try { - const roomId = await client.get("key"); - if (roomId == null) { - // Find a cool server that can handle it - await client.set("key", "value"); - } else { - // Return the server ip in a cookie - } - } catch (e) { - console.error(e); + if (!req.body) { + res.status(400).send("No body"); + return; } + const roomId = req.body.roomId; + if (roomId == undefined || roomMaps[roomId] == undefined) { + res.status(400).send("Invalid room id"); + return; + } + + res.cookie("hostip", roomMaps[roomId]["hostIp"]); + res.cookie("port", roomMaps[roomId]["port"]); + res.status(200).send("here have your server"); + return; }; export default addToRoom; diff --git a/gateway/routes/createRoom.ts b/gateway/routes/createRoom.ts index 017881d..d127567 100644 --- a/gateway/routes/createRoom.ts +++ b/gateway/routes/createRoom.ts @@ -1,14 +1,49 @@ import { type Request, type Response } from "express"; -import addToRoom from "./addToRoom"; -const createRoom = async (req: Request, res: Response) => { - const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; - let code = ""; - for (let i = 0; i < 5; i++) - code += chars[Math.floor(Math.random() * chars.length)]; - - //TODO: Add check for room code in redis - req.cookies.roomId = code; - await addToRoom(req, res); +import generateRoom from "../helper/generateRoom"; +import roomMaps from "../index.ts"; +const createRoom = async (_: Request, res: Response) => { + let code: string; + + // Generate a unqiue room code + do { + code = generateRoom(); + } while (roomMaps[code] != undefined); + + const serverManager = process.env.SERVER_MANAGER_URL; + + try { + // Get assigned a server + const response = await fetch(`${serverManager}/assign`); + + // Validate response + if ( + response.headers.get("hostip") == null || + response.headers.get("port") == null + ) { + res.status(400).send("Couldn't get servers"); + return; + } + + let roomInfo = { + hostIp: "", + port: 0, + }; + roomInfo.hostIp = response.headers.get("hostip")!; + roomInfo.port = Number(response.headers.get("port")); + + // Add roominfo in the map + // TODO: Instead of using a map, use a key value store like Redis + roomMaps[code] = roomInfo; + + // Set cookies + res.cookie("hostIp", roomInfo.hostIp); + res.cookie("port", roomInfo.port); + res.cookie("roomID", code); + res.status(200).send("Here have your server"); + } catch (e) { + console.error(e); + res.status(400).send("Bad request"); + } }; export default createRoom; diff --git a/gateway/types/server.ts b/gateway/types/server.ts new file mode 100644 index 0000000..db1d3d6 --- /dev/null +++ b/gateway/types/server.ts @@ -0,0 +1,7 @@ +interface Server { + hostIp: string; + port: number; + connections: number; +} + +export type { Server }; diff --git a/k8s.yaml b/k8s.yaml new file mode 100644 index 0000000..2492114 --- /dev/null +++ b/k8s.yaml @@ -0,0 +1,324 @@ +# ============================================================================= +# BoxGame – Kubernetes Manifests +# ============================================================================= +# Topology +# [Vercel client] +# │ HTTP / WebSocket +# ▼ +# [gateway] ─────────────────────────────► [server-manager] +# │ sets cookies (hostIp, port, roomID) │ +# │ │ spawns / tracks +# │ ▼ +# └──────────────────────────────────── [game-server pods] +# (hostNetwork, dynamic port) +# +# Namespaces +# box-game – gateway + server-manager +# box-game-servers – dynamically created game-server pods +# ============================================================================= + +--- +# ─── Namespace: core services ───────────────────────────────────────────────── +apiVersion: v1 +kind: Namespace +metadata: + name: box-game + +--- +# ─── Namespace: game server pods ────────────────────────────────────────────── +apiVersion: v1 +kind: Namespace +metadata: + name: box-game-servers + +--- +# ============================================================================= +# RBAC – server-manager needs to create/delete Pods in box-game-servers +# ============================================================================= + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: server-manager-sa + namespace: box-game + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: pod-manager + namespace: box-game-servers +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "create", "delete", "patch"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: server-manager-pod-manager + namespace: box-game-servers +subjects: + - kind: ServiceAccount + name: server-manager-sa + namespace: box-game +roleRef: + kind: Role + name: pod-manager + apiGroup: rbac.authorization.k8s.io + +--- +# ============================================================================= +# SERVER-MANAGER +# ============================================================================= + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: server-manager + namespace: box-game + labels: + app: server-manager +spec: + replicas: 1 + selector: + matchLabels: + app: server-manager + template: + metadata: + labels: + app: server-manager + spec: + serviceAccountName: server-manager-sa + containers: + - name: server-manager + image: docker.io/shadyggs/boxgame-server-manager:latest + imagePullPolicy: Always + ports: + - containerPort: 4689 + name: http + env: + - name: PORT + value: "4689" + readinessProbe: + httpGet: + path: /health + port: 4689 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: 4689 + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "256Mi" + +--- +apiVersion: v1 +kind: Service +metadata: + name: server-manager-svc + namespace: box-game + labels: + app: server-manager +spec: + selector: + app: server-manager + ports: + - port: 4689 + targetPort: 4689 + protocol: TCP + name: http + type: ClusterIP + +--- +# ============================================================================= +# GATEWAY +# ============================================================================= + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gateway + namespace: box-game + labels: + app: gateway +spec: + replicas: 2 + selector: + matchLabels: + app: gateway + template: + metadata: + labels: + app: gateway + spec: + containers: + - name: gateway + image: docker.io/shadyggs/boxgame-gateway:latest + imagePullPolicy: Always + ports: + - containerPort: 3000 + name: http + env: + - name: PORT + value: "3000" + # Talk to server-manager inside the cluster + - name: SERVER_MANAGER_URL + value: "http://server-manager-svc.box-game.svc.cluster.local:4689" + readinessProbe: + httpGet: + path: /healthz + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: 3000 + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "256Mi" + +--- +apiVersion: v1 +kind: Service +metadata: + name: gateway-svc + namespace: box-game + labels: + app: gateway +spec: + selector: + app: gateway + ports: + - port: 80 + targetPort: 3000 + protocol: TCP + name: http + # Change to LoadBalancer if your cluster is on a cloud provider + type: LoadBalancer + +--- +# ============================================================================= +# GAME-SERVER POD TEMPLATE (used by server-manager to spawn pods) +# ============================================================================= +# This ConfigMap stores the pod spec that server-manager reads at runtime +# to create new game-server pods dynamically via the k8s API. +# Update the image name below to match your actual registry tag. +# ============================================================================= + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: game-server-pod-template + namespace: box-game +data: + template.yaml: | + apiVersion: v1 + kind: Pod + metadata: + namespace: box-game-servers + labels: + app: game-server + spec: + # hostNetwork gives the pod a real node-level port so clients + # can connect to it directly via the node's external IP. + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + restartPolicy: Never + containers: + - name: game-server + image: docker.io/shadyggs/boxgame-server:latest + imagePullPolicy: Always + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: SERVER_MANAGER_URL + value: "http://server-manager-svc.box-game.svc.cluster.local:4689" + resources: + requests: + cpu: "200m" + memory: "256Mi" + limits: + cpu: "1000m" + memory: "512Mi" + +--- +# ============================================================================= +# GAME-SERVER – Pre-scaled Deployment +# ============================================================================= +# Each pod starts, listens on a random port (listen(0)), then self-registers +# with server-manager at /register. server-manager's heap is populated this +# way so /assign can immediately return a server. +# +# Increase replicas to pre-warm more server capacity. +# ============================================================================= + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: game-server + namespace: box-game-servers + labels: + app: game-server +spec: + replicas: 3 + selector: + matchLabels: + app: game-server + template: + metadata: + labels: + app: game-server + spec: + # hostNetwork so the socket.io port is reachable from outside the cluster + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + containers: + - name: game-server + image: docker.io/shadyggs/boxgame-server:latest + imagePullPolicy: Always + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: SERVER_MANAGER_URL + value: "http://server-manager-svc.box-game.svc.cluster.local:4689" + # No fixed containerPort – the server calls listen(0) for a random port + resources: + requests: + cpu: "200m" + memory: "256Mi" + limits: + cpu: "1000m" + memory: "512Mi" + readinessProbe: + httpGet: + path: /status + port: 6000 # initial probe port; actual port is random — remove if causing issues + initialDelaySeconds: 3 + periodSeconds: 10 + failureThreshold: 3 diff --git a/server-manager/Dockerfile b/server-manager/Dockerfile new file mode 100644 index 0000000..7503dd5 --- /dev/null +++ b/server-manager/Dockerfile @@ -0,0 +1,19 @@ +# ---- Server-Manager Dockerfile ---- +# Uses Bun to run TypeScript directly (no build step needed) +FROM oven/bun:1-alpine AS base + +WORKDIR /app + +# Install dependencies +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile --production + +# Copy source +COPY . . + +# server-manager talks to the k8s API server in-cluster, so it needs +# the kubeconfig or in-cluster service account credentials (mounted by k8s) +ENV PORT=4689 +EXPOSE 4689 + +CMD ["bun", "run", "index.ts"] diff --git a/server-manager/helper/heap.ts b/server-manager/helper/heap.ts index cc0a455..4468b91 100644 --- a/server-manager/helper/heap.ts +++ b/server-manager/helper/heap.ts @@ -25,6 +25,7 @@ class Heap { this.heap[b] = temp!; } + // Bubble up used for pushing bubbleUp(i: number) { while (i > 0) { const parent = Math.floor((i - 1) / 2); @@ -37,6 +38,7 @@ class Heap { } } + // Bubble down used for popping bubbleDown(i: number) { while (true) { let smallest = i; diff --git a/server-manager/routes/handleRegister.ts b/server-manager/routes/handleRegister.ts index 71fdc6d..223c8e8 100644 --- a/server-manager/routes/handleRegister.ts +++ b/server-manager/routes/handleRegister.ts @@ -12,7 +12,7 @@ const handleRegister = (req: Request, res: Response) => { res.status(401).send("Invalid server info"); return; } - const server = req.body.serverInfo; + const server = req.body; // server.ts sends flat: { hostIp, port, connections } if (!server.hostIp || !server.port) { res.status(401).send("Invalid server info"); return; diff --git a/server/Dockerfile b/server/Dockerfile index d3a6b8b..0d88996 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,13 +1,30 @@ -FROM node:lts-alpine3.24 +# ---- Server Dockerfile ---- +# Multi-stage: compile TS → JS, then run with Node (CommonJS) +FROM node:lts-alpine AS builder -WORKDIR /server - -COPY ./package.json ./package-lock.json ./server.js . +WORKDIR /build +COPY package.json package-lock.json ./ RUN npm ci -ENV PORT=6000 +# Copy source and compile +COPY server.ts ./ +RUN npx tsc --target ES2020 --module commonjs --esModuleInterop true \ + --resolveJsonModule true --skipLibCheck true --outDir dist server.ts + +# ---- Runtime stage ---- +FROM node:lts-alpine AS runtime + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +COPY --from=builder /build/dist/server.js ./server.js -EXPOSE 6000 +# The server listens on a random port (listen(0)) and self-registers +# with server-manager. HOST_IP and SERVER_MANAGER_URL are injected by k8s. +ENV HOST_IP="" +ENV SERVER_MANAGER_URL="http://server-manager-svc.box-game.svc.cluster.local:4689" CMD ["node", "server.js"] diff --git a/server/bun.lock b/server/bun.lock new file mode 100644 index 0000000..198a598 --- /dev/null +++ b/server/bun.lock @@ -0,0 +1,268 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "test-server", + "dependencies": { + "@types/express": "^5.0.6", + "@types/socket.io": "^3.0.2", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "nodemon": "^3.1.11", + "socket.io": "^4.8.3", + }, + }, + }, + "packages": { + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + + "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], + + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + + "@types/socket.io": ["@types/socket.io@3.0.2", "", { "dependencies": { "socket.io": "*" } }, "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "engine.io": ["engine.io@6.6.5", "", { "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io-parser": "~5.2.1", "ws": "~8.18.3" } }, "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A=="], + + "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "nodemon": ["nodemon@3.1.11", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": "bin/nodemon.js" }, "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="], + + "qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], + + "socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="], + + "socket.io-adapter": ["socket.io-adapter@2.5.6", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" } }, "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ=="], + + "socket.io-parser": ["socket.io-parser@4.2.5", "", { "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.4.1" } }, "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + + "engine.io/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "socket.io/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "engine.io/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "engine.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "socket.io/accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "socket.io/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "engine.io/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "socket.io/accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + } +} diff --git a/server/config.yaml b/server/config.yaml index 7c7640b..79a7f78 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -1,2 +1,22 @@ -# This is the kubernetes config for the server pods -# Configure this to bind the pods to it's node's port +apiVersion: v1 +kind: Pod +metadata: + name: game-server + namespace: box-game-servers + +spec: + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + + containers: + - name: game-server + image: your-image:latest + + env: + - name: HOST_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + + - name: SERVER_MANAGER_URL + value: http://localhost:4289 diff --git a/server/package-lock.json b/server/package-lock.json index dbde319..e737783 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,10 +9,13 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/express": "^5.0.6", + "@types/socket.io": "^3.0.2", "dotenv": "^17.3.1", "express": "^5.2.1", "nodemon": "^3.1.11", - "socket.io": "^4.8.3" + "socket.io": "^4.8.3", + "typescript": "^6.0.3" } }, "node_modules/@socket.io/component-emitter": { @@ -21,6 +24,25 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cors": { "version": "2.8.19", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", @@ -30,6 +52,35 @@ "@types/node": "*" } }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.2.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", @@ -39,6 +90,47 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/socket.io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz", + "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==", + "deprecated": "This is a stub types definition. socket.io provides its own type definitions, so you do not need this installed.", + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1343,6 +1435,19 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/server/package.json b/server/package.json index 4e223e0..768b538 100644 --- a/server/package.json +++ b/server/package.json @@ -11,9 +11,12 @@ "license": "ISC", "type": "commonjs", "dependencies": { + "@types/express": "^5.0.6", + "@types/socket.io": "^3.0.2", "dotenv": "^17.3.1", "express": "^5.2.1", "nodemon": "^3.1.11", - "socket.io": "^4.8.3" + "socket.io": "^4.8.3", + "typescript": "^6.0.3" } } diff --git a/server/server.js b/server/server.ts similarity index 55% rename from server/server.js rename to server/server.ts index 9d3ff8e..0cd50b3 100644 --- a/server/server.js +++ b/server/server.ts @@ -1,12 +1,14 @@ -require("dotenv").config(); +import express from "express"; +import http from "http"; +import { AddressInfo } from "net"; +import { Server } from "socket.io"; -const express = require("express"); -const http = require("http"); -const { Server } = require("socket.io"); +// Register the server to server-manager // Create server const app = express(); const server = http.createServer(app); +// This io creates a new socket connection for the clients to connect to const io = new Server(server, { cors: { origin: "*" }, pingInterval: 10000, @@ -15,10 +17,9 @@ const io = new Server(server, { }); // Interaction constants -const port = parseInt(process.env.PORT) || 3000; const RES_WIDTH = 1280; const RES_HEIGHT = 720; -const tick = parseInt(process.env.TICK, 10) || 120; +const tick = 120; // Game constants const gravity = 0.5; @@ -30,12 +31,21 @@ const MAX_PLAYERS_PER_ROOM = 20; // Color palette for players const playerColors = [ - "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", - "#FFEAA7", "#DDA0DD", "#98D8C8", "#F7DC6F", - "#BB8FCE", "#85C1E9", "#F0B27A", "#82E0AA", + "#FF6B6B", + "#4ECDC4", + "#45B7D1", + "#96CEB4", + "#FFEAA7", + "#DDA0DD", + "#98D8C8", + "#F7DC6F", + "#BB8FCE", + "#85C1E9", + "#F0B27A", + "#82E0AA", ]; -function getRandomBetween(min, max) { +function getRandomBetween(min: number, max: number) { return Math.random() * (max - min) + min; } @@ -81,9 +91,15 @@ function buildPlatformGrid() { for (let i = 0; i < world.platforms.length; i++) { const p = world.platforms[i]; const startCol = Math.max(0, Math.floor(p.x / GRID_CELL_SIZE)); - const endCol = Math.min(GRID_COLS - 1, Math.floor((p.x + p.width) / GRID_CELL_SIZE)); + const endCol = Math.min( + GRID_COLS - 1, + Math.floor((p.x + p.width) / GRID_CELL_SIZE), + ); const startRow = Math.max(0, Math.floor(p.y / GRID_CELL_SIZE)); - const endRow = Math.min(GRID_ROWS - 1, Math.floor((p.y + p.height) / GRID_CELL_SIZE)); + const endRow = Math.min( + GRID_ROWS - 1, + Math.floor((p.y + p.height) / GRID_CELL_SIZE), + ); for (let row = startRow; row <= endRow; row++) { for (let col = startCol; col <= endCol; col++) { platformGrid[row * GRID_COLS + col].push(world.platforms[i]); @@ -93,13 +109,45 @@ function buildPlatformGrid() { } buildPlatformGrid(); -const _seen = []; -const _result = []; -function getNearbyPlatforms(player) { +const _seen: Platform[] = []; +const _result: Platform[] = []; + +interface Rectangle { + x: number; + y: number; + width: number; + height: number; +} + +type Platform = Rectangle; + +interface Target extends Rectangle {} + +interface Player extends Rectangle { + id: string; + color: string; + velocityX: number; + velocityY: number; + speed: number; + isGrounded: boolean; + name: string; + score: number; + highScore: number; + inputLeft: boolean; + inputRight: boolean; + inputJump: boolean; +} +function getNearbyPlatforms(player: Player): Platform[] { const startCol = Math.max(0, Math.floor(player.x / GRID_CELL_SIZE)); - const endCol = Math.min(GRID_COLS - 1, Math.floor((player.x + player.width) / GRID_CELL_SIZE)); + const endCol = Math.min( + GRID_COLS - 1, + Math.floor((player.x + player.width) / GRID_CELL_SIZE), + ); const startRow = Math.max(0, Math.floor(player.y / GRID_CELL_SIZE)); - const endRow = Math.min(GRID_ROWS - 1, Math.floor((player.y + player.height) / GRID_CELL_SIZE)); + const endRow = Math.min( + GRID_ROWS - 1, + Math.floor((player.y + player.height) / GRID_CELL_SIZE), + ); _seen.length = 0; _result.length = 0; for (let row = startRow; row <= endRow; row++) { @@ -107,7 +155,10 @@ function getNearbyPlatforms(player) { const cell = platformGrid[row * GRID_COLS + col]; for (let i = 0; i < cell.length; i++) { const plat = cell[i]; - if (_seen.indexOf(plat) === -1) { _seen.push(plat); _result.push(plat); } + if (_seen.indexOf(plat) === -1) { + _seen.push(plat); + _result.push(plat); + } } } } @@ -116,37 +167,69 @@ function getNearbyPlatforms(player) { // ============== ROOM MANAGEMENT ============== // rooms[roomCode] = { players, target, lastSentState, fullSyncCounter, pendingScoreEvents } -const rooms = {}; +// +// Rooms interface +interface Target { + x: number; + y: number; + width: number; + height: number; +} -function generateRoomCode() { - const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; - let code = ""; - for (let i = 0; i < 5; i++) code += chars[Math.floor(Math.random() * chars.length)]; - return code; +interface ScoreEvent { + playerName: string; + score: number; + x: number; + y: number; } -function createRoom() { - let code; - do { code = generateRoomCode(); } while (rooms[code]); - - rooms[code] = { - players: {}, - playerCount: 0, - target: { - x: Math.random() * 1280, - y: getRandomBetween(40, 250), - color: "green", - width: 10, - height: 10, - }, - lastSentState: {}, - fullSyncCounter: 0, - pendingScoreEvents: [], - }; - return code; +interface CachedPlayerState { + x: number; + y: number; + score: number; + name: string; } -function deleteRoomIfEmpty(code) { +interface Player { + id: string; + x: number; + y: number; + width: number; + height: number; + color: string; + velocityX: number; + velocityY: number; + speed: number; + isGrounded: boolean; + name: string; + score: number; + highScore: number; + inputLeft: boolean; + inputRight: boolean; + inputJump: boolean; +} + +interface Room { + players: Record; + playerCount: number; + target: Target; + lastSentState: Record; + fullSyncCounter: number; + pendingScoreEvents: ScoreEvent[]; +} +interface ClientPlayerState extends Rectangle { + id: string; + color: string; + velocityX: number; + velocityY: number; + name: string; + score: number; +} +type Rooms = Record; + +const rooms: Rooms = {}; + +function deleteRoomIfEmpty(code: string) { const room = rooms[code]; if (room && room.playerCount === 0) { delete rooms[code]; @@ -155,12 +238,16 @@ function deleteRoomIfEmpty(code) { } // ============== GAME LOGIC ============== -function isColliding(a, b) { - return a.x + a.width > b.x && a.x < b.x + b.width && - a.y + a.height > b.y && a.y < b.y + b.height; +function isColliding(a: Rectangle, b: Rectangle) { + return ( + a.x + a.width > b.x && + a.x < b.x + b.width && + a.y + a.height > b.y && + a.y < b.y + b.height + ); } -function respawnPlayer(player) { +function respawnPlayer(player: Player) { player.x = RESPAWN_X; player.y = RESPAWN_Y; player.velocityX = 0; @@ -170,15 +257,25 @@ function respawnPlayer(player) { player.score = 0; } -function checkBounds(player) { - if (player.x + player.width > RES_WIDTH) { player.x = RES_WIDTH - player.width; player.velocityX = 0; } - if (player.x < 0) { player.x = 0; player.velocityX = 0; } +function checkBounds(player: Player) { + if (player.x + player.width > RES_WIDTH) { + player.x = RES_WIDTH - player.width; + player.velocityX = 0; + } + if (player.x < 0) { + player.x = 0; + player.velocityX = 0; + } if (player.y > RES_HEIGHT) respawnPlayer(player); - if (player.y < 0) { player.y = 0; player.velocityY = 0; } + if (player.y < 0) { + player.y = 0; + player.velocityY = 0; + } } -function repositionTarget(target) { - let valid = false, attempts = 0; +function repositionTarget(target: Target) { + let valid = false, + attempts = 0; while (!valid && attempts < 50) { target.x = Math.random() * (RES_WIDTH - target.width); target.y = getRandomBetween(40, 250); @@ -186,30 +283,43 @@ function repositionTarget(target) { attempts++; for (let i = 0; i < world.platforms.length; i++) { const p = world.platforms[i]; - if (target.x + target.width > p.x && target.x < p.x + p.width && - target.y + target.height > p.y && target.y < p.y + p.height) { - valid = false; break; + if ( + target.x + target.width > p.x && + target.x < p.x + p.width && + target.y + target.height > p.y && + target.y < p.y + p.height + ) { + valid = false; + break; } } } } -function checkTargetCollision(player, room) { +function checkTargetCollision(player: Player, room: Room) { if (isColliding(player, room.target)) { player.score++; if (player.score > player.highScore) player.highScore = player.score; - room.pendingScoreEvents.push({ playerName: player.name, score: player.score, x: room.target.x, y: room.target.y }); + room.pendingScoreEvents.push({ + playerName: player.name, + score: player.score, + x: room.target.x, + y: room.target.y, + }); repositionTarget(room.target); } } -function applyGravity(player) { +function applyGravity(player: Player) { player.velocityY += gravity; player.y += player.velocityY; player.x += player.velocityX; player.isGrounded = false; if (isColliding(player, world.ground)) { - if (player.y + player.height > world.ground.y && player.y < world.ground.y) { + if ( + player.y + player.height > world.ground.y && + player.y < world.ground.y + ) { player.velocityY = 0; player.y = world.ground.y - player.height; player.isGrounded = true; @@ -217,65 +327,115 @@ function applyGravity(player) { } } -function checkPlatformCollision(player, platform) { +function checkPlatformCollision(player: Player, platform: Rectangle) { if (!isColliding(player, platform)) return; - const overlapLeft = player.x + player.width - platform.x; - const overlapRight = platform.x + platform.width - player.x; - const overlapTop = player.y + player.height - platform.y; + const overlapLeft = player.x + player.width - platform.x; + const overlapRight = platform.x + platform.width - player.x; + const overlapTop = player.y + player.height - platform.y; const overlapBottom = platform.y + platform.height - player.y; - const minOverlap = Math.min(overlapLeft, overlapRight, overlapTop, overlapBottom); - if (minOverlap === overlapTop && player.velocityY >= 0) { player.y = platform.y - player.height; player.velocityY = 0; player.isGrounded = true; } - else if (minOverlap === overlapBottom && player.velocityY < 0) { player.y = platform.y + platform.height; player.velocityY = 0; } - else if (minOverlap === overlapLeft) { player.x = platform.x - player.width; player.velocityX = 0; } - else if (minOverlap === overlapRight) { player.x = platform.x + platform.width; player.velocityX = 0; } + const minOverlap = Math.min( + overlapLeft, + overlapRight, + overlapTop, + overlapBottom, + ); + if (minOverlap === overlapTop && player.velocityY >= 0) { + player.y = platform.y - player.height; + player.velocityY = 0; + player.isGrounded = true; + } else if (minOverlap === overlapBottom && player.velocityY < 0) { + player.y = platform.y + platform.height; + player.velocityY = 0; + } else if (minOverlap === overlapLeft) { + player.x = platform.x - player.width; + player.velocityX = 0; + } else if (minOverlap === overlapRight) { + player.x = platform.x + platform.width; + player.velocityX = 0; + } } -function processInputs(player) { +function processInputs(player: Player) { player.velocityX = 0; - if (player.inputLeft) player.velocityX = -player.speed; - if (player.inputRight) player.velocityX = player.speed; - if (player.inputJump && player.isGrounded) { player.velocityY = jumpPower; player.isGrounded = false; } + if (player.inputLeft) player.velocityX = -player.speed; + if (player.inputRight) player.velocityX = player.speed; + if (player.inputJump && player.isGrounded) { + player.velocityY = jumpPower; + player.isGrounded = false; + } } -function updateRoom(room) { +function updateRoom(room: Room) { for (const id in room.players) { const player = room.players[id]; processInputs(player); applyGravity(player); const nearby = getNearbyPlatforms(player); - for (let i = 0; i < nearby.length; i++) checkPlatformCollision(player, nearby[i]); + for (let i = 0; i < nearby.length; i++) + checkPlatformCollision(player, nearby[i]); checkBounds(player); checkTargetCollision(player, room); } } -function buildClientState(room) { - const slim = {}; +function buildClientState(room: Room) { + const slim: Record = {}; for (const id in room.players) { const p = room.players[id]; - slim[id] = { id: p.id, x: p.x, y: p.y, width: p.width, height: p.height, color: p.color, velocityX: p.velocityX, velocityY: p.velocityY, name: p.name, score: p.score }; + slim[id] = { + id: p.id, + x: p.x, + y: p.y, + width: p.width, + height: p.height, + color: p.color, + velocityX: p.velocityX, + velocityY: p.velocityY, + name: p.name, + score: p.score, + }; } return slim; } -function buildDeltaState(room) { - const delta = {}; +function buildDeltaState(room: Room) { + const delta: Record = {}; let hasChanges = false; for (const id in room.players) { const p = room.players[id]; const last = room.lastSentState[id]; - if (!last || Math.abs(p.x - last.x) > 0.5 || Math.abs(p.y - last.y) > 0.5 || p.score !== last.score || p.name !== last.name) { - delta[id] = { id: p.id, x: Math.round(p.x * 10) / 10, y: Math.round(p.y * 10) / 10, width: p.width, height: p.height, color: p.color, velocityX: Math.round(p.velocityX * 10) / 10, velocityY: Math.round(p.velocityY * 10) / 10, name: p.name, score: p.score }; + if ( + !last || + Math.abs(p.x - last.x) > 0.5 || + Math.abs(p.y - last.y) > 0.5 || + p.score !== last.score || + p.name !== last.name + ) { + delta[id] = { + id: p.id, + x: Math.round(p.x * 10) / 10, + y: Math.round(p.y * 10) / 10, + width: p.width, + height: p.height, + color: p.color, + velocityX: Math.round(p.velocityX * 10) / 10, + velocityY: Math.round(p.velocityY * 10) / 10, + name: p.name, + score: p.score, + }; hasChanges = true; } } for (const id in room.lastSentState) { - if (!room.players[id]) { delta[id] = null; hasChanges = true; } + if (!room.players[id]) { + delta[id] = null; + hasChanges = true; + } } return hasChanges ? delta : null; } -function cacheRoomState(room) { +function cacheRoomState(room: Room) { room.lastSentState = {}; for (const id in room.players) { const p = room.players[id]; @@ -285,18 +445,18 @@ function cacheRoomState(room) { // ============== SOCKET CONNECTION ============== io.on("connection", (socket) => { - let currentRoomCode = null; + let currentRoomCode: string | null = null; console.log(`Connected: ${socket.id}`); // ---- CREATE ROOM ---- - socket.on("createRoom", (playerName, callback) => { + socket.on("createRoom", (playerName, roomId, callback) => { if (currentRoomCode) { // Leave existing room first leaveCurrentRoom(); } - const code = createRoom(); + const code = roomId; currentRoomCode = code; socket.join(code); @@ -315,11 +475,13 @@ io.on("connection", (socket) => { const room = rooms[code]; if (!room) { - if (typeof callback === "function") callback({ success: false, error: "Room not found." }); + if (typeof callback === "function") + callback({ success: false, error: "Room not found." }); return; } if (room.playerCount >= MAX_PLAYERS_PER_ROOM) { - if (typeof callback === "function") callback({ success: false, error: "Room is full." }); + if (typeof callback === "function") + callback({ success: false, error: "Room is full." }); return; } @@ -343,7 +505,9 @@ io.on("connection", (socket) => { const player = rooms[currentRoomCode].players[socket.id]; if (!player) return; player.name = sanitizeName(newName); - console.log(`${socket.id} changed name to "${player.name}" in room ${currentRoomCode}`); + console.log( + `${socket.id} changed name to "${player.name}" in room ${currentRoomCode}`, + ); }); // ---- INPUTS ---- @@ -351,9 +515,9 @@ io.on("connection", (socket) => { if (!currentRoomCode || !rooms[currentRoomCode]) return; const player = rooms[currentRoomCode].players[socket.id]; if (!player || !input) return; - player.inputLeft = !!input.left; + player.inputLeft = !!input.left; player.inputRight = !!input.right; - player.inputJump = !!input.jump; + player.inputJump = !!input.jump; }); // Legacy "name" event for compatibility @@ -395,7 +559,11 @@ setInterval(() => { if (room.fullSyncCounter >= tick * 2) { room.fullSyncCounter = 0; const fullState = buildClientState(room); - io.to(code).emit("gameState", { target: room.target, players: fullState, full: true }); + io.to(code).emit("gameState", { + target: room.target, + players: fullState, + full: true, + }); cacheRoomState(room); } else { const delta = buildDeltaState(room); @@ -415,13 +583,13 @@ setInterval(() => { }, 1000 / tick); // ============== HELPERS ============== -function sanitizeName(name) { +function sanitizeName(name: string) { if (typeof name !== "string") return "Player"; const s = name.trim().slice(0, 15); return s.length === 0 ? "Player" : s; } -function makePlayer(id, name) { +function makePlayer(id: string, name: string) { return { id, x: RESPAWN_X, @@ -443,12 +611,26 @@ function makePlayer(id, name) { } // Status endpoint -app.get("/status", (req, res) => { - const totalPlayers = Object.values(rooms).reduce((acc, r) => acc + r.playerCount, 0); - res.json({ rooms: Object.keys(rooms).length, players: totalPlayers, uptime: process.uptime() }); +app.get("/status", () => { + console.log("Server is upppp"); }); -server.listen(port, () => { - console.log(`Server running on http://localhost:${port}`); +server.listen(0, async () => { + const address = server.address() as AddressInfo; + const serverInfo = { + hostIp: process.env.HOST_IP, + port: address.port, + connections: 0, + }; + + await fetch(`${process.env.SERVER_MANAGER_URL}/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(serverInfo), + }); + + console.log(`Server running on http://localhost:${address.port}`); console.log(`Tick rate: ${tick}`); });