import express from "express"; import path from "node:path"; import { PORT, PUBLIC_ROOT } from "./config.js"; import { BuildManager } from "./lib/build-manager.js"; import { detectCatalog, getGameConfig } from "./lib/catalog.js"; const app = express(); const catalog = detectCatalog(); const builds = new BuildManager(catalog); app.disable("x-powered-by"); app.use(express.json({ limit: "64kb" })); app.use(express.static(PUBLIC_ROOT, { extensions: ["html"] })); app.get("/api/maps", (_request, response) => { response.json(builds.listCatalog()); }); app.post("/api/builds", async (request, response) => { try { const game = String(request.body?.game ?? ""); const mapId = Number.parseInt(String(request.body?.mapId ?? ""), 10); const options = { includeEditor: request.body?.includeEditor !== false, includeRoofs: request.body?.includeRoofs === true }; const gameConfig = getGameConfig(game); if (!gameConfig) { response.status(400).json({ error: "Unknown game id" }); return; } if (!Number.isInteger(mapId) || mapId < 0) { response.status(400).json({ error: "Invalid map id" }); return; } const job = await builds.createOrReuseBuild(gameConfig, mapId, options); response.status(202).json(builds.getPublicJob(job)); } catch (error) { response.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); app.get("/api/builds/:id", (request, response) => { const job = builds.getJob(request.params.id); if (!job) { response.status(404).json({ error: "Unknown build id" }); return; } response.json(builds.getPublicJob(job)); }); app.get("/api/maps/:game/:mapId/metadata", (request, response) => { try { const buildId = String(request.query.buildId ?? ""); const mapId = Number.parseInt(request.params.mapId, 10); const metadata = builds.getMetadata(buildId, request.params.game, mapId); response.json(metadata); } catch (error) { response.status(400).json({ error: error instanceof Error ? error.message : String(error) }); } }); app.get("/api/maps/:game/:mapId/tiles/:tileX/:tileY.png", (request, response) => { try { const buildId = String(request.query.buildId ?? ""); const mapId = Number.parseInt(request.params.mapId, 10); const tileX = Number.parseInt(request.params.tileX, 10); const tileY = Number.parseInt(request.params.tileY, 10); if (!Number.isInteger(tileX) || !Number.isInteger(tileY) || tileX < 0 || tileY < 0) { response.status(400).json({ error: "Invalid tile coordinates" }); return; } const png = builds.renderTile(buildId, request.params.game, mapId, tileX, tileY); response.setHeader("Content-Type", "image/png"); response.setHeader("Cache-Control", "public, max-age=31536000, immutable"); response.end(png); } catch (error) { response.status(400).json({ error: error instanceof Error ? error.message : String(error) }); } }); app.get("/api/maps/:game/:mapId/download.png", async (request, response) => { try { const buildId = String(request.query.buildId ?? ""); const mapId = Number.parseInt(request.params.mapId, 10); const filePath = await builds.renderFullMap(buildId, request.params.game, mapId); response.setHeader("Content-Type", "image/png"); response.setHeader("Content-Disposition", `attachment; filename="${path.basename(filePath)}"`); response.sendFile(path.resolve(filePath), { dotfiles: "allow" }); } catch (error) { response.status(400).json({ error: error instanceof Error ? error.message : String(error) }); } }); app.get("/api/health", (_request, response) => { response.json({ ok: true, games: catalog.games.length }); }); app.listen(PORT, () => { console.log(`Crusader map renderer listening on http://localhost:${PORT}`); });