mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 01:47:43 +00:00
251 lines
6.6 KiB
TypeScript
251 lines
6.6 KiB
TypeScript
import compression from "compression";
|
|
import express, { Request, Response } from "express";
|
|
import { dirname, join } from "path";
|
|
import { fileURLToPath } from "url";
|
|
import {
|
|
clearCache as clearMapCache,
|
|
getMapMetadata,
|
|
listMaps,
|
|
} from "./api/maps.js";
|
|
import { clearAdapterCaches, computePath } from "./api/pathfinding.js";
|
|
import { computeSpatialQuery } from "./api/spatialQuery.js";
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT ?? 5555;
|
|
|
|
// Middleware
|
|
app.use(compression()); // gzip compression for large responses
|
|
app.use(express.json({ limit: "50mb" })); // JSON body parser with larger limit
|
|
|
|
// Serve static files from public directory
|
|
const publicDir = join(dirname(fileURLToPath(import.meta.url)), "public");
|
|
app.use(express.static(publicDir));
|
|
|
|
// API Routes
|
|
|
|
/**
|
|
* GET /api/maps
|
|
* List all available maps
|
|
*/
|
|
app.get("/api/maps", (req: Request, res: Response) => {
|
|
try {
|
|
const maps = listMaps();
|
|
res.json({ maps });
|
|
} catch (error) {
|
|
console.error("Error listing maps:", error);
|
|
res.status(500).json({
|
|
error: "Failed to list maps",
|
|
message: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/maps/:name
|
|
* Get map metadata (map data, dimensions)
|
|
*/
|
|
app.get("/api/maps/:name", async (req: Request, res: Response) => {
|
|
try {
|
|
const { name } = req.params;
|
|
const metadata = await getMapMetadata(name);
|
|
res.json(metadata);
|
|
} catch (error) {
|
|
console.error(`Error loading map ${req.params.name}:`, error);
|
|
|
|
if (error instanceof Error && error.message.includes("ENOENT")) {
|
|
res.status(404).json({
|
|
error: "Map not found",
|
|
message: `Map "${req.params.name}" does not exist`,
|
|
});
|
|
} else {
|
|
res.status(500).json({
|
|
error: "Failed to load map",
|
|
message: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /api/maps/:name/thumbnail
|
|
* Get map thumbnail image
|
|
*/
|
|
app.get("/api/maps/:name/thumbnail", (req: Request, res: Response) => {
|
|
try {
|
|
const { name } = req.params;
|
|
const thumbnailPath = join(
|
|
dirname(fileURLToPath(import.meta.url)),
|
|
"../../../resources/maps",
|
|
name,
|
|
"thumbnail.webp",
|
|
);
|
|
res.sendFile(thumbnailPath);
|
|
} catch (error) {
|
|
console.error(`Error loading thumbnail for ${req.params.name}:`, error);
|
|
res.status(404).json({
|
|
error: "Thumbnail not found",
|
|
message: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/pathfind
|
|
* Compute pathfinding between two points
|
|
*
|
|
* Request body:
|
|
* {
|
|
* map: string,
|
|
* from: [x, y],
|
|
* to: [x, y],
|
|
* adapters?: string[] // Optional: which comparison adapters to run
|
|
* }
|
|
*
|
|
* Response:
|
|
* {
|
|
* primary: { path, length, time, debug: { nodePath, initialPath, timings } },
|
|
* comparisons: [{ adapter, path, length, time }, ...]
|
|
* }
|
|
*/
|
|
app.post("/api/pathfind", async (req: Request, res: Response) => {
|
|
try {
|
|
const { map, from, to, adapters } = req.body;
|
|
|
|
// Validate request
|
|
if (!map || !from || !to) {
|
|
return res.status(400).json({
|
|
error: "Invalid request",
|
|
message: "Missing required fields: map, from, to",
|
|
});
|
|
}
|
|
|
|
if (
|
|
!Array.isArray(from) ||
|
|
from.length !== 2 ||
|
|
!Array.isArray(to) ||
|
|
to.length !== 2
|
|
) {
|
|
return res.status(400).json({
|
|
error: "Invalid coordinates",
|
|
message: "from and to must be [x, y] coordinate arrays",
|
|
});
|
|
}
|
|
|
|
// Compute paths
|
|
const result = await computePath(
|
|
map,
|
|
from as [number, number],
|
|
to as [number, number],
|
|
{ adapters },
|
|
);
|
|
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error("Error computing path:", error);
|
|
|
|
if (error instanceof Error && error.message.includes("is not water")) {
|
|
res.status(400).json({
|
|
error: "Invalid coordinates",
|
|
message: error.message,
|
|
});
|
|
} else {
|
|
res.status(500).json({
|
|
error: "Failed to compute path",
|
|
message: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/spatial-query
|
|
* Compute spatial query for transport ship (closestShoreByWater)
|
|
*
|
|
* Request body:
|
|
* {
|
|
* map: string,
|
|
* ownedTiles: number[], // Array of tile indices (y * width + x)
|
|
* target: [x, y]
|
|
* }
|
|
*/
|
|
app.post("/api/spatial-query", async (req: Request, res: Response) => {
|
|
try {
|
|
const { map, ownedTiles, target } = req.body;
|
|
|
|
if (!map || !ownedTiles || !target) {
|
|
return res.status(400).json({
|
|
error: "Invalid request",
|
|
message: "Missing required fields: map, ownedTiles, target",
|
|
});
|
|
}
|
|
|
|
if (!Array.isArray(ownedTiles)) {
|
|
return res.status(400).json({
|
|
error: "Invalid ownedTiles",
|
|
message: "ownedTiles must be an array of tile indices",
|
|
});
|
|
}
|
|
|
|
if (!Array.isArray(target) || target.length !== 2) {
|
|
return res.status(400).json({
|
|
error: "Invalid target",
|
|
message: "target must be [x, y] coordinate array",
|
|
});
|
|
}
|
|
|
|
const result = await computeSpatialQuery(
|
|
map,
|
|
ownedTiles,
|
|
target as [number, number],
|
|
);
|
|
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error("Error computing spatial query:", error);
|
|
res.status(500).json({
|
|
error: "Failed to compute spatial query",
|
|
message: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/cache/clear
|
|
* Clear all caches (useful for development)
|
|
*/
|
|
app.post("/api/cache/clear", (req: Request, res: Response) => {
|
|
try {
|
|
clearMapCache();
|
|
clearAdapterCaches();
|
|
res.json({ message: "Caches cleared successfully" });
|
|
} catch (error) {
|
|
console.error("Error clearing caches:", error);
|
|
res.status(500).json({
|
|
error: "Failed to clear caches",
|
|
message: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
});
|
|
|
|
// Error handling middleware
|
|
app.use((err: Error, req: Request, res: Response, next: any) => {
|
|
console.error("Unhandled error:", err);
|
|
res.status(500).json({
|
|
error: "Internal server error",
|
|
message: err.message,
|
|
});
|
|
});
|
|
|
|
// Start server
|
|
app.listen(PORT, () => {
|
|
console.log(`
|
|
╔════════════════════════════════════════════════════════════╗
|
|
║ Pathfinding Playground Server ║
|
|
╚════════════════════════════════════════════════════════════╝
|
|
|
|
Server running at: http://localhost:${PORT}
|
|
|
|
Press Ctrl+C to stop
|
|
`);
|
|
});
|