mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-10 18:24:38 +00:00
a7f992e9b0
Restructure the single src/ tree into an npm-workspaces monorepo under
packages/, rename core -> engine, extract a types-only core-public layer,
and break the pre-existing engine -> client dependency cycle.
Structure (packages/):
core-public public API/wire schemas + shared enums (clean leaf)
shared framework-agnostic helpers (clean leaf)
engine deterministic simulation (was src/core)
client rendering/UI (was src/client)
server coordination (was src/server)
Dependency DAG: engine -> {core-public, shared}; client -> {core-public,
shared, engine}; server -> {core-public, engine}.
- npm workspaces: root package.json workspaces + per-package package.json;
tsconfig.base.json holds shared options + path aliases
(core-public/* shared/* engine/* client/* server/*) resolved uniformly by
tsc, Vite (resolve.tsconfigPaths), Vitest, and tsx. Lockfile regenerated.
- core-public: moved Schemas/ApiSchemas/CosmeticSchemas/StatsSchemas/
ClanApiSchemas/WorkerSchemas/Base64/PatternDecoder; extracted the enums
(GameTypes), GameEvent type, emoji table, and GraphicsOverrides schema.
Engine re-exports the moved enums/types so existing imports keep working.
- Broke engine -> client cycle:
- renderNumber/renderTroops -> shared/format
- NameBoxCalculator moved into engine
- username validation returns translation key + params; client translates
- applyStateUpdate moved to client (operates on the render-only PlayerState)
- Config/UnitGrid/execution-Util/GameImpl now use structural read
interfaces (engine/game/ReadViews: PlayerLike/UnitLike/GameLike) instead
of importing client view classes; client imports view classes from a new
client/view barrel; deleted the engine/game/GameView re-export shim.
- Build/deploy updated: vite.config, index.html, eslint, Dockerfile
(copies packages/ + tsconfig.base.json before npm ci), .vscode, tests.
Verified: tsc --noEmit clean; 1364 + 65 tests pass; production vite build
succeeds; engine has zero client/server imports; core-public and shared are
dependency leaves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
261 lines
6.8 KiB
TypeScript
261 lines
6.8 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<{ name: string }>, 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<{ name: string }>, 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, (error?: Error) => {
|
|
if (error) {
|
|
console.error("Failed to start server", error);
|
|
process.exit(1);
|
|
}
|
|
console.log(`
|
|
╔════════════════════════════════════════════════════════════╗
|
|
║ Pathfinding Playground Server ║
|
|
╚════════════════════════════════════════════════════════════╝
|
|
|
|
Server running at: http://localhost:${PORT}
|
|
|
|
Press Ctrl+C to stop
|
|
`);
|
|
});
|