Spaces:
Sleeping
Sleeping
Implement terrain, map coordinate, and GLB collision upgrades, configure WebSockets keepalive, and fix client MapSchema callbacks TypeError
93f151d | import express from "express"; | |
| import http from "http"; | |
| import path from "path"; | |
| import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; | |
| import { createServer as createViteServer } from "vite"; | |
| import { Server, matchMaker } from "@colyseus/core"; | |
| import { WebSocketTransport } from "@colyseus/ws-transport"; | |
| import { MultiplayerRoom } from "./src/game/MultiplayerRoom"; | |
| async function startServer() { | |
| const app = express(); | |
| app.use(express.json()); | |
| const PORT = parseInt(process.env.PORT ?? "7860", 10); | |
| const server = http.createServer(app); | |
| // Initialize Colyseus game server with keepalive pings so the HF proxy | |
| // does not terminate idle WebSocket connections. | |
| const gameServer = new Server({ | |
| transport: new WebSocketTransport({ server, pingInterval: 8000, pingMaxRetries: 3 }) | |
| }); | |
| gameServer.define("air_combat", MultiplayerRoom); | |
| // Mount Colyseus matchmaking routes (/matchmake/*) onto this Express app. | |
| // In Colyseus 0.17 the transport owns an internal express router; exposing it | |
| // here is the documented way to share an existing http.Server. | |
| const colyseusApp = | |
| (gameServer as any).expressApp ?? | |
| (gameServer as any).transport?.expressApp ?? | |
| null; | |
| if (colyseusApp) { | |
| app.use("/", colyseusApp); | |
| } else { | |
| console.warn("[Server] gameServer.expressApp unavailable — matchmaking routes may not be registered"); | |
| (gameServer as any).bindRoutes?.(); | |
| } | |
| // Authoritative progression save directory | |
| const saveDir = existsSync("/data") ? "/data/saves" : path.join(process.cwd(), "saves"); | |
| if (!existsSync(saveDir)) { | |
| try { mkdirSync(saveDir, { recursive: true }); } catch (e) { console.error(e); } | |
| } | |
| // Save/Load Progression APIs | |
| app.get("/api/progression", (req, res) => { | |
| const sid = req.query.sid as string | undefined; | |
| if (!sid || sid.length > 128) return res.status(400).json({ error: "Invalid session ID" }); | |
| const filePath = path.join(saveDir, `${sid}.json`); | |
| if (existsSync(filePath)) { | |
| try { res.json(JSON.parse(readFileSync(filePath, "utf-8"))); } catch { res.status(500).json({ error: "Failed to read save" }); } | |
| } else { | |
| res.json({ status: "not_found" }); | |
| } | |
| }); | |
| app.post("/api/progression", (req, res) => { | |
| const { sessionId, progression } = req.body; | |
| if (!sessionId || sessionId.length > 128 || !progression) return res.status(400).json({ error: "Invalid request payload" }); | |
| try { | |
| writeFileSync(path.join(saveDir, `${sessionId}.json`), JSON.stringify(progression, null, 2), "utf-8"); | |
| res.json({ ok: true }); | |
| } catch { | |
| res.status(500).json({ error: "Failed to save progression" }); | |
| } | |
| }); | |
| // Lobby presence: token -> last-seen timestamp. Entries expire after 15s. | |
| const lobbyPresence = new Map<string, number>(); | |
| app.get("/api/presence", (req, res) => res.json({ ok: true })); | |
| app.get("/api/ice-servers", (req, res) => res.json([{ urls: "stun:stun.l.google.com:19302" }])); | |
| app.post("/api/client-error", (req, res) => { | |
| console.error("[Client Diagnostic Error]:", req.body); | |
| res.json({ ok: true }); | |
| }); | |
| app.get("/api/health", async (req, res) => { | |
| const token = (req.query.t as string) || req.ip || "anon"; | |
| const now = Date.now(); | |
| lobbyPresence.set(token, now); | |
| const cutoff = now - 15000; | |
| for (const [k, v] of lobbyPresence) if (v < cutoff) lobbyPresence.delete(k); | |
| try { | |
| const rooms = await matchMaker.query({ name: "air_combat" }); | |
| let roomPlayers = 0; | |
| rooms.forEach((r) => roomPlayers += r.clients); | |
| const totalPlayers = roomPlayers + lobbyPresence.size; | |
| res.json({ status: "ok", totalPlayers, byQueue: {} }); | |
| } catch { | |
| res.json({ status: "ok", totalPlayers: lobbyPresence.size, byQueue: {} }); | |
| } | |
| }); | |
| app.get("/api/preview", (req, res) => { | |
| res.json({ players: [] }); | |
| }); | |
| // Serve client assets | |
| if (process.env.NODE_ENV !== "production") { | |
| const vite = await createViteServer({ server: { middlewareMode: true }, appType: "spa" }); | |
| app.use(vite.middlewares); | |
| } else { | |
| const distPath = path.join(process.cwd(), "dist"); | |
| app.use(express.static(distPath)); | |
| app.get("*", (req, res) => res.sendFile(path.join(distPath, "index.html"))); | |
| } | |
| server.listen(PORT, "0.0.0.0", () => { | |
| console.log(`[Airframe Server Auth] Online on http://localhost:${PORT}`); | |
| }); | |
| } | |
| startServer().catch(console.error); | |