const http = require("http"); const fs = require("fs"); const path = require("path"); const { URL } = require("url"); const repoRoot = path.resolve(__dirname, "..", ".."); const port = Number(process.env.MAP_EDITOR_PORT || process.env.PORT || 8787); const contentTypes = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".map": "text/plain; charset=utf-8", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".json": "application/json; charset=utf-8", }; const sourceMapPath = path.join(repoRoot, "game", "assets", "map", "stage_01.map"); const deployTargets = [ sourceMapPath, path.join(repoRoot, "build", "windows", "x64", "release", "assets", "map", "stage_01.map"), path.join(repoRoot, "build", "switch", "aarch64", "release", "switch_game", "assets", "map", "stage_01.map"), path.join(repoRoot, "build", "switch", "aarch64", "release", "switch_game", "romfs", "assets", "map", "stage_01.map"), ]; const server = http.createServer(async (request, response) => { try { const url = new URL(request.url, `http://${request.headers.host || "localhost"}`); if (url.pathname === "/api/map-editor/status" && request.method === "GET") { sendJson(response, 200, { ok: true, writable: true }); return; } if (url.pathname === "/api/map-editor/save" && request.method === "POST") { const { mapText } = await readJsonBody(request); const targets = writeMapToTargets(mapText, [sourceMapPath]); sendJson(response, 200, { ok: true, targets }); return; } if (url.pathname === "/api/map-editor/deploy" && request.method === "POST") { const { mapText } = await readJsonBody(request); const targets = writeMapToTargets(mapText, deployTargets, { skipMissingParents: true }); sendJson(response, 200, { ok: true, targets }); return; } if (request.method !== "GET" && request.method !== "HEAD") { sendJson(response, 405, { ok: false, error: "Method not allowed" }); return; } serveStatic(url.pathname, request, response); } catch (error) { sendJson(response, 500, { ok: false, error: error.message }); } }); server.listen(port, "127.0.0.1", () => { console.log(`Map editor: http://127.0.0.1:${port}/tools/map_editor/`); }); function writeMapToTargets(mapText, targets, options = {}) { validateMapText(mapText); const written = []; for (const target of targets) { const parent = path.dirname(target); if (options.skipMissingParents && !fs.existsSync(parent)) continue; fs.mkdirSync(parent, { recursive: true }); fs.writeFileSync(target, normalizeText(mapText), "utf8"); written.push(path.relative(repoRoot, target).replace(/\\/g, "/")); } return written; } function validateMapText(mapText) { if (typeof mapText !== "string" || !mapText.trim()) { throw new Error("mapText is empty"); } if (!/^map\s+\S+/m.test(mapText) || !/^world\s+[-.\d]+\s+[-.\d]+/m.test(mapText)) { throw new Error("mapText does not look like a .map file"); } } function normalizeText(text) { return `${text.replace(/\r\n/g, "\n").replace(/\s+$/g, "")}\n`; } function serveStatic(urlPath, request, response) { const pathname = decodeURIComponent(urlPath === "/" ? "/tools/map_editor/" : urlPath); const relative = pathname.replace(/^\/+/, ""); const target = path.resolve(repoRoot, relative); if (!target.startsWith(repoRoot + path.sep) && target !== repoRoot) { sendText(response, 403, "Forbidden"); return; } let filePath = target; if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) { filePath = path.join(filePath, "index.html"); } if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { sendText(response, 404, "Not found"); return; } response.writeHead(200, { "Content-Type": contentTypes[path.extname(filePath).toLowerCase()] || "application/octet-stream", "Cache-Control": "no-store", }); if (request.method === "HEAD") { response.end(); return; } fs.createReadStream(filePath).pipe(response); } function readJsonBody(request) { return new Promise((resolve, reject) => { let body = ""; request.setEncoding("utf8"); request.on("data", (chunk) => { body += chunk; if (body.length > 1024 * 1024) { reject(new Error("Request body too large")); request.destroy(); } }); request.on("end", () => { try { resolve(JSON.parse(body || "{}")); } catch (error) { reject(new Error("Invalid JSON body")); } }); request.on("error", reject); }); } function sendJson(response, status, payload) { response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", }); response.end(JSON.stringify(payload)); } function sendText(response, status, text) { response.writeHead(status, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", }); response.end(text); }