102 lines
3.8 KiB
JavaScript
102 lines
3.8 KiB
JavaScript
|
|
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}`);
|
||
|
|
});
|