commit a5f686510fe0eed2cdabae71738600575e6e150f Author: FlatHack Date: Thu Jun 18 10:27:16 2026 +0200 Initial REM planning editor diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a98f70f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM node:24-alpine + +WORKDIR /app + +COPY package.json ./ +COPY server.mjs ./ +COPY star-map.html ./ + +ENV HOST=0.0.0.0 +ENV PORT=3000 +ENV DATA_DIR=/data + +VOLUME ["/data"] +EXPOSE 3000 + +CMD ["node", "server.mjs"] diff --git a/build-universe-map.mjs b/build-universe-map.mjs new file mode 100644 index 0000000..6955798 --- /dev/null +++ b/build-universe-map.mjs @@ -0,0 +1,257 @@ +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const projectDir = dirname(fileURLToPath(import.meta.url)); + +const systemNames = { + li01: "New York", + li02: "California", + li03: "Colorado", + li04: "Texas", + li05: "Alaska", + li06: "New Nevada", + li07: "Puerto Rico", + br01: "New London", + br02: "Cambridge", + br03: "Manchester", + br04: "Leeds", + br05: "Edinburgh", + br06: "Dublin", + ku01: "New Tokyo", + ku02: "Shikoku", + ku03: "Kyushu", + ku04: "Honshu", + ku05: "Hokkaido", + ku06: "Chugoku", + ku07: "Tohoku", + rh01: "New Berlin", + rh02: "Frankfurt", + rh03: "Stuttgart", + rh04: "Hamburg", + rh05: "Dresden", + iw01: "Bering", + iw02: "Hudson", + iw03: "Magellan", + iw04: "Cortez", + iw05: "Kepler", + iw06: "Galileo", + bw01: "Omega-3", + bw02: "Omega-5", + bw03: "Omega-7", + bw04: "Omega-11", + bw05: "Omega-41", + bw06: "Sigma-13", + bw07: "Sigma-17", + bw08: "Sigma-19", + bw09: "Tau-31", + bw10: "Tau-29", + bw11: "Sigma 2", + ew01: "Tau-37", + ew02: "Omicron Alpha", + ew03: "Omicron Gamma", + ew04: "Omicron Theta", + ew05: "Unknown Gamma", + ew06: "Unknown Delta", + hi01: "Unknown Alpha", + hi02: "Omicron Beta", + st01: "Omicron Minor", + st02: "Omicron Beta", + st02c: "Unknown Beta", + st03: "Omicron Major", + st03b: "Omicron Major", + st04: "Omicron Lambda", + st05: "Omicron Prime", + fp7_system: "Omicron Major" +}; + +function stripComment(line) { + return line.replace(/;.*/, "").trim(); +} + +function parseIniBlocks(text) { + const blocks = []; + let current = null; + + for (const rawLine of text.split(/\r?\n/)) { + const line = stripComment(rawLine); + if (!line) { + continue; + } + + const header = line.match(/^\[([^\]]+)]$/); + if (header) { + current = { section: header[1].toLowerCase(), values: {} }; + blocks.push(current); + continue; + } + + if (!current) { + continue; + } + + const pair = line.match(/^([^=]+?)\s*=\s*(.*)$/); + if (!pair) { + continue; + } + const key = pair[1].trim().toLowerCase(); + const value = pair[2].trim(); + if (!current.values[key]) { + current.values[key] = []; + } + current.values[key].push(value); + } + + return blocks; +} + +function firstValue(block, key) { + return block.values[key]?.[0]; +} + +function parseGridPosition(value) { + const parts = value.split(",").map((part) => Number(part.trim())); + if (parts.length < 2 || !Number.isFinite(parts[0]) || !Number.isFinite(parts[1])) { + throw new Error(`Invalid system pos: ${value}`); + } + return { gridX: parts[0], gridY: parts[1] }; +} + +function makeConnectionId(from, to) { + return `c-${from}-${to}`; +} + +function normalizePair(from, to) { + return [from, to].sort((a, b) => a.localeCompare(b)).join("|"); +} + +async function readDirectConnections(universeDir, systemsById) { + const connections = []; + const seen = new Set(); + + for (const system of systemsById.values()) { + const systemFile = resolve(universeDir, system.file.replaceAll("\\", "/")); + let text; + try { + text = await readFile(systemFile, "utf8"); + } catch { + continue; + } + + for (const block of parseIniBlocks(text)) { + if (block.section !== "object") { + continue; + } + const gotoValues = block.values.goto || []; + const archetype = (firstValue(block, "archetype") || "").toLowerCase(); + const type = archetype.includes("hole") ? "neutral" : "standard"; + + for (const gotoValue of gotoValues) { + const targetId = gotoValue.split(",")[0]?.trim().toLowerCase(); + if (!targetId || !systemsById.has(targetId) || targetId === system.id) { + continue; + } + const key = normalizePair(system.id, targetId); + if (seen.has(key)) { + continue; + } + seen.add(key); + const [from, to] = key.split("|"); + connections.push({ + id: makeConnectionId(from, to), + from, + to, + type + }); + } + } + } + + return connections.sort((a, b) => a.id.localeCompare(b.id)); +} + +export async function buildUniverseMap(options = {}) { + const universeDir = options.universeDir || "C:/PROJECTS/REM/REM-Mod/DATA/UNIVERSE"; + const universePath = join(universeDir, "universe.ini"); + const text = await readFile(universePath, "utf8"); + const systemBlocks = parseIniBlocks(text).filter((block) => block.section === "system"); + const parsedSystems = systemBlocks.map((block) => { + const nickname = firstValue(block, "nickname"); + const file = firstValue(block, "file"); + const pos = firstValue(block, "pos"); + if (!nickname || !file || !pos) { + throw new Error("System block is missing nickname/file/pos"); + } + const id = nickname.toLowerCase(); + const { gridX, gridY } = parseGridPosition(pos); + return { + id, + nickname, + name: systemNames[id] || nickname, + file, + gridX, + gridY, + stridName: Number(firstValue(block, "strid_name") || 0), + navMapScale: Number(firstValue(block, "navmapscale") || 1), + visit: Number(firstValue(block, "visit") || 0), + note: "" + }; + }); + + const minCol = Math.min(...parsedSystems.map((system) => system.gridX)); + const minRow = Math.min(...parsedSystems.map((system) => system.gridY)); + const maxCol = Math.max(...parsedSystems.map((system) => system.gridX)); + const maxRow = Math.max(...parsedSystems.map((system) => system.gridY)); + const cellSize = 72; + const margin = 72; + const grid = { minCol, minRow, maxCol, maxRow, cellSize, margin }; + + const systems = parsedSystems.map((system) => ({ + ...system, + x: margin + (system.gridX - minCol) * cellSize, + y: margin + (system.gridY - minRow) * cellSize + })).sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id)); + + const systemsById = new Map(systems.map((system) => [system.id, system])); + const connections = await readDirectConnections(universeDir, systemsById); + + return { + source: "universe.ini", + background: "#05080f", + grid, + viewBox: { + x: 0, + y: 0, + width: margin * 2 + (maxCol - minCol) * cellSize, + height: margin * 2 + (maxRow - minRow) * cellSize + }, + systems, + connections + }; +} + +async function updateProjectFiles() { + const mapData = await buildUniverseMap(); + const htmlPath = join(projectDir, "star-map.html"); + const dataPath = join(projectDir, "data", "map.json"); + const html = await readFile(htmlPath, "utf8"); + const json = JSON.stringify(mapData, null, 2); + const nextHtml = html.replace( + /` + ); + + if (nextHtml === html) { + throw new Error("Could not replace initial-map-data in star-map.html"); + } + + await writeFile(htmlPath, nextHtml, "utf8"); + await mkdir(dirname(dataPath), { recursive: true }); + await writeFile(dataPath, `${json}\n`, "utf8"); + return mapData; +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const mapData = await updateProjectFiles(); + console.log(`Imported ${mapData.systems.length} systems and ${mapData.connections.length} connections from universe.ini`); +} diff --git a/data/map.json b/data/map.json new file mode 100644 index 0000000..231a7c3 --- /dev/null +++ b/data/map.json @@ -0,0 +1,1434 @@ +{ + "source": "universe.ini", + "background": "#05080f", + "grid": { + "minCol": 0, + "minRow": 0, + "maxCol": 16, + "maxRow": 16, + "cellSize": 72, + "margin": 72 + }, + "viewBox": { + "x": 0, + "y": 0, + "width": 1296, + "height": 1296 + }, + "systems": [ + { + "id": "li05", + "nickname": "Li05", + "name": "Alaska", + "file": "systems\\Li05\\Li05.ini", + "gridX": 9, + "gridY": 7, + "stridName": 196613, + "navMapScale": 1.5, + "visit": 0, + "note": "", + "x": 720, + "y": 576 + }, + { + "id": "iw01", + "nickname": "Iw01", + "name": "Bering", + "file": "systems\\Iw01\\Iw01.ini", + "gridX": 9, + "gridY": 9, + "stridName": 196643, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 720, + "y": 720 + }, + { + "id": "li02", + "nickname": "Li02", + "name": "California", + "file": "systems\\Li02\\Li02.ini", + "gridX": 6, + "gridY": 9, + "stridName": 196610, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 504, + "y": 720 + }, + { + "id": "br02", + "nickname": "Br02", + "name": "Cambridge", + "file": "systems\\Br02\\Br02.ini", + "gridX": 4, + "gridY": 10, + "stridName": 196615, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 360, + "y": 792 + }, + { + "id": "ku06", + "nickname": "Ku06", + "name": "Chugoku", + "file": "systems\\Ku06\\Ku06.ini", + "gridX": 7, + "gridY": 2, + "stridName": 196631, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 576, + "y": 216 + }, + { + "id": "li03", + "nickname": "Li03", + "name": "Colorado", + "file": "systems\\Li03\\Li03.ini", + "gridX": 7, + "gridY": 7, + "stridName": 196611, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 576, + "y": 576 + }, + { + "id": "iw04", + "nickname": "Iw04", + "name": "Cortez", + "file": "systems\\Iw04\\Iw04.ini", + "gridX": 5, + "gridY": 9, + "stridName": 196646, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 432, + "y": 720 + }, + { + "id": "rh05", + "nickname": "Rh05", + "name": "Dresden", + "file": "systems\\Rh05\\Rh05.ini", + "gridX": 11, + "gridY": 12, + "stridName": 196624, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 864, + "y": 936 + }, + { + "id": "br06", + "nickname": "Br06", + "name": "Dublin", + "file": "systems\\Br06\\Br06.ini", + "gridX": 0, + "gridY": 10, + "stridName": 196619, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 72, + "y": 792 + }, + { + "id": "br05", + "nickname": "Br05", + "name": "Edinburgh", + "file": "systems\\Br05\\Br05.ini", + "gridX": 1, + "gridY": 13, + "stridName": 196618, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 144, + "y": 1008 + }, + { + "id": "rh02", + "nickname": "Rh02", + "name": "Frankfurt", + "file": "systems\\Rh02\\Rh02.ini", + "gridX": 10, + "gridY": 10, + "stridName": 196621, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 792, + "y": 792 + }, + { + "id": "iw06", + "nickname": "Iw06", + "name": "Galileo", + "file": "systems\\Iw06\\Iw06.ini", + "gridX": 8, + "gridY": 6, + "stridName": 196648, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 648, + "y": 504 + }, + { + "id": "rh04", + "nickname": "Rh04", + "name": "Hamburg", + "file": "systems\\Rh04\\Rh04.ini", + "gridX": 12, + "gridY": 10, + "stridName": 196623, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 936, + "y": 792 + }, + { + "id": "ku05", + "nickname": "Ku05", + "name": "Hokkaido", + "file": "systems\\Ku05\\Ku05.ini", + "gridX": 7, + "gridY": 3, + "stridName": 196630, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 576, + "y": 288 + }, + { + "id": "ku04", + "nickname": "Ku04", + "name": "Honshu", + "file": "systems\\Ku04\\Ku04.ini", + "gridX": 8, + "gridY": 4, + "stridName": 196629, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 648, + "y": 360 + }, + { + "id": "iw02", + "nickname": "Iw02", + "name": "Hudson", + "file": "systems\\Iw02\\Iw02.ini", + "gridX": 9, + "gridY": 10, + "stridName": 196644, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 720, + "y": 792 + }, + { + "id": "iw05", + "nickname": "Iw05", + "name": "Kepler", + "file": "systems\\Iw05\\Iw05.ini", + "gridX": 6, + "gridY": 6, + "stridName": 196647, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 504, + "y": 504 + }, + { + "id": "ku03", + "nickname": "Ku03", + "name": "Kyushu", + "file": "systems\\Ku03\\Ku03.ini", + "gridX": 6, + "gridY": 4, + "stridName": 196628, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 504, + "y": 360 + }, + { + "id": "br04", + "nickname": "Br04", + "name": "Leeds", + "file": "systems\\Br04\\Br04.ini", + "gridX": 2, + "gridY": 9, + "stridName": 196617, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 216, + "y": 720 + }, + { + "id": "iw03", + "nickname": "Iw03", + "name": "Magellan", + "file": "systems\\Iw03\\Iw03.ini", + "gridX": 5, + "gridY": 10, + "stridName": 196645, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 432, + "y": 792 + }, + { + "id": "br03", + "nickname": "Br03", + "name": "Manchester", + "file": "systems\\Br03\\Br03.ini", + "gridX": 3, + "gridY": 12, + "stridName": 196616, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 288, + "y": 936 + }, + { + "id": "rh01", + "nickname": "Rh01", + "name": "New Berlin", + "file": "systems\\Rh01\\Rh01.ini", + "gridX": 11, + "gridY": 11, + "stridName": 196620, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 864, + "y": 864 + }, + { + "id": "br01", + "nickname": "Br01", + "name": "New London", + "file": "systems\\Br01\\Br01.ini", + "gridX": 2, + "gridY": 11, + "stridName": 196614, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 216, + "y": 864 + }, + { + "id": "li06", + "nickname": "LI06", + "name": "New Nevada", + "file": "systems\\LI06\\LI06.ini", + "gridX": 6, + "gridY": 8, + "stridName": 450000, + "navMapScale": 1.82, + "visit": 1, + "note": "", + "x": 504, + "y": 648 + }, + { + "id": "ku01", + "nickname": "Ku01", + "name": "New Tokyo", + "file": "systems\\Ku01\\Ku01.ini", + "gridX": 7, + "gridY": 4, + "stridName": 196626, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 576, + "y": 360 + }, + { + "id": "li01", + "nickname": "Li01", + "name": "New York", + "file": "Systems\\Li01\\Li01.ini", + "gridX": 7, + "gridY": 8, + "stridName": 196609, + "navMapScale": 1, + "visit": 1, + "note": "", + "x": 576, + "y": 648 + }, + { + "id": "bw04", + "nickname": "Bw04", + "name": "Omega-11", + "file": "systems\\Bw04\\Bw04.ini", + "gridX": 8, + "gridY": 13, + "stridName": 196636, + "navMapScale": 2.5, + "visit": 0, + "note": "", + "x": 648, + "y": 1008 + }, + { + "id": "bw01", + "nickname": "Bw01", + "name": "Omega-3", + "file": "systems\\Bw01\\Bw01.ini", + "gridX": 5, + "gridY": 12, + "stridName": 196633, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 432, + "y": 936 + }, + { + "id": "bw05", + "nickname": "Bw05", + "name": "Omega-41", + "file": "systems\\Bw05\\Bw05.ini", + "gridX": 11, + "gridY": 6, + "stridName": 196637, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 864, + "y": 504 + }, + { + "id": "bw02", + "nickname": "Bw02", + "name": "Omega-5", + "file": "systems\\Bw02\\Bw02.ini", + "gridX": 5, + "gridY": 14, + "stridName": 196634, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 432, + "y": 1080 + }, + { + "id": "bw03", + "nickname": "Bw03", + "name": "Omega-7", + "file": "systems\\Bw03\\Bw03.ini", + "gridX": 7, + "gridY": 12, + "stridName": 196635, + "navMapScale": 3, + "visit": 0, + "note": "", + "x": 576, + "y": 936 + }, + { + "id": "ew02", + "nickname": "Ew02", + "name": "Omicron Alpha", + "file": "systems\\Ew02\\Ew02.ini", + "gridX": 10, + "gridY": 1, + "stridName": 196650, + "navMapScale": 4, + "visit": 0, + "note": "", + "x": 792, + "y": 144 + }, + { + "id": "hi02", + "nickname": "Hi02", + "name": "Omicron Beta", + "file": "systems\\Hi02\\Hi02.ini", + "gridX": 13, + "gridY": 14, + "stridName": 196654, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 1008, + "y": 1080 + }, + { + "id": "st02", + "nickname": "St02", + "name": "Omicron Beta", + "file": "systems\\St02\\St02.ini", + "gridX": 13, + "gridY": 4, + "stridName": 196656, + "navMapScale": 2, + "visit": 128, + "note": "", + "x": 1008, + "y": 360 + }, + { + "id": "ew03", + "nickname": "Ew03", + "name": "Omicron Gamma", + "file": "systems\\Ew03\\Ew03.ini", + "gridX": 10, + "gridY": 14, + "stridName": 196651, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 792, + "y": 1080 + }, + { + "id": "st04", + "nickname": "ST04", + "name": "Omicron Lambda", + "file": "systems\\St04\\St04.ini", + "gridX": 15, + "gridY": 5, + "stridName": 198004, + "navMapScale": 2.125, + "visit": 0, + "note": "", + "x": 1152, + "y": 432 + }, + { + "id": "fp7_system", + "nickname": "FP7_system", + "name": "Omicron Major", + "file": "systems\\FP7\\FP7_system.ini", + "gridX": 16, + "gridY": 6, + "stridName": 196657, + "navMapScale": 3, + "visit": 128, + "note": "", + "x": 1224, + "y": 504 + }, + { + "id": "st03", + "nickname": "St03", + "name": "Omicron Major", + "file": "systems\\St03\\St03.ini", + "gridX": 14, + "gridY": 3, + "stridName": 196657, + "navMapScale": 2, + "visit": 128, + "note": "", + "x": 1080, + "y": 288 + }, + { + "id": "st03b", + "nickname": "St03b", + "name": "Omicron Major", + "file": "systems\\St03b\\St03b.ini", + "gridX": 15, + "gridY": 3, + "stridName": 196657, + "navMapScale": 2, + "visit": 128, + "note": "", + "x": 1152, + "y": 288 + }, + { + "id": "st01", + "nickname": "St01", + "name": "Omicron Minor", + "file": "systems\\St01\\St01.ini", + "gridX": 14, + "gridY": 5, + "stridName": 196655, + "navMapScale": 2, + "visit": 128, + "note": "", + "x": 1080, + "y": 432 + }, + { + "id": "st05", + "nickname": "St05", + "name": "Omicron Prime", + "file": "systems\\ST05\\st05.ini", + "gridX": 12, + "gridY": 16, + "stridName": 454900, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 936, + "y": 1224 + }, + { + "id": "ew04", + "nickname": "Ew04", + "name": "Omicron Theta", + "file": "systems\\Ew04\\Ew04.ini", + "gridX": 15, + "gridY": 12, + "stridName": 196652, + "navMapScale": 2.5, + "visit": 0, + "note": "", + "x": 1152, + "y": 936 + }, + { + "id": "li07", + "nickname": "LI07", + "name": "Puerto Rico", + "file": "systems\\LI07\\LI07.ini", + "gridX": 4, + "gridY": 7, + "stridName": 450100, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 360, + "y": 576 + }, + { + "id": "ku02", + "nickname": "Ku02", + "name": "Shikoku", + "file": "systems\\Ku02\\Ku02.ini", + "gridX": 7, + "gridY": 5, + "stridName": 196627, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 576, + "y": 432 + }, + { + "id": "bw11", + "nickname": "Bw11", + "name": "Sigma 2", + "file": "systems\\Bw11\\Bw11.ini", + "gridX": 10, + "gridY": 7, + "stridName": 196608, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 792, + "y": 576 + }, + { + "id": "bw06", + "nickname": "Bw06", + "name": "Sigma-13", + "file": "systems\\Bw06\\Bw06.ini", + "gridX": 12, + "gridY": 6, + "stridName": 196638, + "navMapScale": 2.5, + "visit": 0, + "note": "", + "x": 936, + "y": 504 + }, + { + "id": "bw07", + "nickname": "Bw07", + "name": "Sigma-17", + "file": "systems\\Bw07\\Bw07.ini", + "gridX": 11, + "gridY": 4, + "stridName": 196639, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 864, + "y": 360 + }, + { + "id": "bw08", + "nickname": "Bw08", + "name": "Sigma-19", + "file": "systems\\Bw08\\Bw08.ini", + "gridX": 2, + "gridY": 5, + "stridName": 196640, + "navMapScale": 3, + "visit": 0, + "note": "", + "x": 216, + "y": 432 + }, + { + "id": "rh03", + "nickname": "Rh03", + "name": "Stuttgart", + "file": "systems\\Rh03\\Rh03.ini", + "gridX": 10, + "gridY": 11, + "stridName": 196622, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 792, + "y": 864 + }, + { + "id": "bw10", + "nickname": "Bw10", + "name": "Tau-29", + "file": "systems\\Bw10\\Bw10.ini", + "gridX": 2, + "gridY": 7, + "stridName": 196642, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 216, + "y": 576 + }, + { + "id": "bw09", + "nickname": "Bw09", + "name": "Tau-31", + "file": "systems\\Bw09\\Bw09.ini", + "gridX": 3, + "gridY": 6, + "stridName": 196641, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 288, + "y": 504 + }, + { + "id": "ew01", + "nickname": "Ew01", + "name": "Tau-37", + "file": "systems\\Ew01\\Ew01.ini", + "gridX": 4, + "gridY": 1, + "stridName": 196649, + "navMapScale": 3.5, + "visit": 0, + "note": "", + "x": 360, + "y": 144 + }, + { + "id": "li04", + "nickname": "Li04", + "name": "Texas", + "file": "systems\\Li04\\Li04.ini", + "gridX": 8, + "gridY": 9, + "stridName": 196612, + "navMapScale": 1, + "visit": 0, + "note": "", + "x": 648, + "y": 720 + }, + { + "id": "ku07", + "nickname": "Ku07", + "name": "Tohoku", + "file": "systems\\Ku07\\Ku07.ini", + "gridX": 9, + "gridY": 2, + "stridName": 196632, + "navMapScale": 1.7, + "visit": 0, + "note": "", + "x": 720, + "y": 216 + }, + { + "id": "hi01", + "nickname": "Hi01", + "name": "Unknown Alpha", + "file": "systems\\Hi01\\Hi01.ini", + "gridX": 8, + "gridY": 0, + "stridName": 196653, + "navMapScale": 2, + "visit": 0, + "note": "", + "x": 648, + "y": 72 + }, + { + "id": "st02c", + "nickname": "St02c", + "name": "Unknown Beta", + "file": "systems\\St02c\\St02c.ini", + "gridX": 13, + "gridY": 1, + "stridName": 196657, + "navMapScale": 3, + "visit": 128, + "note": "", + "x": 1008, + "y": 144 + }, + { + "id": "ew06", + "nickname": "Ew06", + "name": "Unknown Delta", + "file": "systems\\Ew06\\Ew06.ini", + "gridX": 15, + "gridY": 9, + "stridName": 198011, + "navMapScale": 3, + "visit": 0, + "note": "", + "x": 1152, + "y": 720 + }, + { + "id": "ew05", + "nickname": "Ew05", + "name": "Unknown Gamma", + "file": "systems\\Ew05\\Ew05.ini", + "gridX": 15, + "gridY": 0, + "stridName": 198010, + "navMapScale": 3, + "visit": 0, + "note": "", + "x": 1152, + "y": 72 + } + ], + "connections": [ + { + "id": "c-br01-br02", + "from": "br01", + "to": "br02", + "type": "standard" + }, + { + "id": "c-br01-br03", + "from": "br01", + "to": "br03", + "type": "standard" + }, + { + "id": "c-br01-br04", + "from": "br01", + "to": "br04", + "type": "standard" + }, + { + "id": "c-br01-br05", + "from": "br01", + "to": "br05", + "type": "standard" + }, + { + "id": "c-br02-br04", + "from": "br02", + "to": "br04", + "type": "neutral" + }, + { + "id": "c-br02-iw03", + "from": "br02", + "to": "iw03", + "type": "standard" + }, + { + "id": "c-br02-iw04", + "from": "br02", + "to": "iw04", + "type": "standard" + }, + { + "id": "c-br03-br04", + "from": "br03", + "to": "br04", + "type": "neutral" + }, + { + "id": "c-br03-bw01", + "from": "br03", + "to": "bw01", + "type": "standard" + }, + { + "id": "c-br03-bw02", + "from": "br03", + "to": "bw02", + "type": "neutral" + }, + { + "id": "c-br04-br05", + "from": "br04", + "to": "br05", + "type": "neutral" + }, + { + "id": "c-br04-br06", + "from": "br04", + "to": "br06", + "type": "standard" + }, + { + "id": "c-br04-bw10", + "from": "br04", + "to": "bw10", + "type": "standard" + }, + { + "id": "c-br04-iw03", + "from": "br04", + "to": "iw03", + "type": "neutral" + }, + { + "id": "c-br06-bw10", + "from": "br06", + "to": "bw10", + "type": "neutral" + }, + { + "id": "c-bw01-bw02", + "from": "bw01", + "to": "bw02", + "type": "neutral" + }, + { + "id": "c-bw01-bw03", + "from": "bw01", + "to": "bw03", + "type": "standard" + }, + { + "id": "c-bw02-bw03", + "from": "bw02", + "to": "bw03", + "type": "neutral" + }, + { + "id": "c-bw02-bw04", + "from": "bw02", + "to": "bw04", + "type": "neutral" + }, + { + "id": "c-bw02-ew03", + "from": "bw02", + "to": "ew03", + "type": "neutral" + }, + { + "id": "c-bw03-bw04", + "from": "bw03", + "to": "bw04", + "type": "neutral" + }, + { + "id": "c-bw03-rh03", + "from": "bw03", + "to": "rh03", + "type": "standard" + }, + { + "id": "c-bw04-ew03", + "from": "bw04", + "to": "ew03", + "type": "neutral" + }, + { + "id": "c-bw04-rh03", + "from": "bw04", + "to": "rh03", + "type": "standard" + }, + { + "id": "c-bw04-rh05", + "from": "bw04", + "to": "rh05", + "type": "neutral" + }, + { + "id": "c-bw05-bw06", + "from": "bw05", + "to": "bw06", + "type": "neutral" + }, + { + "id": "c-bw05-bw07", + "from": "bw05", + "to": "bw07", + "type": "neutral" + }, + { + "id": "c-bw05-bw11", + "from": "bw05", + "to": "bw11", + "type": "neutral" + }, + { + "id": "c-bw05-ku04", + "from": "bw05", + "to": "ku04", + "type": "standard" + }, + { + "id": "c-bw05-ku06", + "from": "bw05", + "to": "ku06", + "type": "neutral" + }, + { + "id": "c-bw05-rh01", + "from": "bw05", + "to": "rh01", + "type": "neutral" + }, + { + "id": "c-bw05-rh04", + "from": "bw05", + "to": "rh04", + "type": "standard" + }, + { + "id": "c-bw06-bw07", + "from": "bw06", + "to": "bw07", + "type": "neutral" + }, + { + "id": "c-bw06-ew04", + "from": "bw06", + "to": "ew04", + "type": "neutral" + }, + { + "id": "c-bw07-ew02", + "from": "bw07", + "to": "ew02", + "type": "neutral" + }, + { + "id": "c-bw07-ku04", + "from": "bw07", + "to": "ku04", + "type": "standard" + }, + { + "id": "c-bw08-bw09", + "from": "bw08", + "to": "bw09", + "type": "neutral" + }, + { + "id": "c-bw08-bw10", + "from": "bw08", + "to": "bw10", + "type": "standard" + }, + { + "id": "c-bw08-ew01", + "from": "bw08", + "to": "ew01", + "type": "neutral" + }, + { + "id": "c-bw08-ku03", + "from": "bw08", + "to": "ku03", + "type": "neutral" + }, + { + "id": "c-bw09-bw10", + "from": "bw09", + "to": "bw10", + "type": "standard" + }, + { + "id": "c-bw09-ku03", + "from": "bw09", + "to": "ku03", + "type": "standard" + }, + { + "id": "c-bw11-li05", + "from": "bw11", + "to": "li05", + "type": "neutral" + }, + { + "id": "c-ew01-hi01", + "from": "ew01", + "to": "hi01", + "type": "neutral" + }, + { + "id": "c-ew01-ku06", + "from": "ew01", + "to": "ku06", + "type": "neutral" + }, + { + "id": "c-ew02-hi01", + "from": "ew02", + "to": "hi01", + "type": "neutral" + }, + { + "id": "c-ew03-ew04", + "from": "ew03", + "to": "ew04", + "type": "neutral" + }, + { + "id": "c-ew03-hi02", + "from": "ew03", + "to": "hi02", + "type": "neutral" + }, + { + "id": "c-ew03-rh05", + "from": "ew03", + "to": "rh05", + "type": "neutral" + }, + { + "id": "c-ew03-st05", + "from": "ew03", + "to": "st05", + "type": "neutral" + }, + { + "id": "c-ew04-hi01", + "from": "ew04", + "to": "hi01", + "type": "neutral" + }, + { + "id": "c-ew04-hi02", + "from": "ew04", + "to": "hi02", + "type": "neutral" + }, + { + "id": "c-ew05-hi01", + "from": "ew05", + "to": "hi01", + "type": "neutral" + }, + { + "id": "c-ew05-st04", + "from": "ew05", + "to": "st04", + "type": "neutral" + }, + { + "id": "c-ew06-hi02", + "from": "ew06", + "to": "hi02", + "type": "neutral" + }, + { + "id": "c-ew06-st04", + "from": "ew06", + "to": "st04", + "type": "standard" + }, + { + "id": "c-iw01-iw02", + "from": "iw01", + "to": "iw02", + "type": "neutral" + }, + { + "id": "c-iw01-li04", + "from": "iw01", + "to": "li04", + "type": "standard" + }, + { + "id": "c-iw01-rh02", + "from": "iw01", + "to": "rh02", + "type": "standard" + }, + { + "id": "c-iw02-li04", + "from": "iw02", + "to": "li04", + "type": "standard" + }, + { + "id": "c-iw02-rh02", + "from": "iw02", + "to": "rh02", + "type": "standard" + }, + { + "id": "c-iw03-iw04", + "from": "iw03", + "to": "iw04", + "type": "neutral" + }, + { + "id": "c-iw03-li01", + "from": "iw03", + "to": "li01", + "type": "standard" + }, + { + "id": "c-iw03-li02", + "from": "iw03", + "to": "li02", + "type": "standard" + }, + { + "id": "c-iw04-li02", + "from": "iw04", + "to": "li02", + "type": "standard" + }, + { + "id": "c-iw04-li06", + "from": "iw04", + "to": "li06", + "type": "neutral" + }, + { + "id": "c-iw05-iw06", + "from": "iw05", + "to": "iw06", + "type": "neutral" + }, + { + "id": "c-iw05-ku02", + "from": "iw05", + "to": "ku02", + "type": "standard" + }, + { + "id": "c-iw05-li03", + "from": "iw05", + "to": "li03", + "type": "standard" + }, + { + "id": "c-iw06-ku02", + "from": "iw06", + "to": "ku02", + "type": "standard" + }, + { + "id": "c-iw06-li03", + "from": "iw06", + "to": "li03", + "type": "standard" + }, + { + "id": "c-ku01-ku02", + "from": "ku01", + "to": "ku02", + "type": "standard" + }, + { + "id": "c-ku01-ku03", + "from": "ku01", + "to": "ku03", + "type": "standard" + }, + { + "id": "c-ku01-ku04", + "from": "ku01", + "to": "ku04", + "type": "standard" + }, + { + "id": "c-ku01-ku05", + "from": "ku01", + "to": "ku05", + "type": "standard" + }, + { + "id": "c-ku02-ku03", + "from": "ku02", + "to": "ku03", + "type": "neutral" + }, + { + "id": "c-ku03-ku05", + "from": "ku03", + "to": "ku05", + "type": "neutral" + }, + { + "id": "c-ku04-ku06", + "from": "ku04", + "to": "ku06", + "type": "neutral" + }, + { + "id": "c-ku05-ku06", + "from": "ku05", + "to": "ku06", + "type": "neutral" + }, + { + "id": "c-ku05-ku07", + "from": "ku05", + "to": "ku07", + "type": "neutral" + }, + { + "id": "c-ku06-ku07", + "from": "ku06", + "to": "ku07", + "type": "neutral" + }, + { + "id": "c-li01-li02", + "from": "li01", + "to": "li02", + "type": "standard" + }, + { + "id": "c-li01-li03", + "from": "li01", + "to": "li03", + "type": "standard" + }, + { + "id": "c-li01-li04", + "from": "li01", + "to": "li04", + "type": "standard" + }, + { + "id": "c-li01-li05", + "from": "li01", + "to": "li05", + "type": "standard" + }, + { + "id": "c-li01-li06", + "from": "li01", + "to": "li06", + "type": "neutral" + }, + { + "id": "c-li02-li04", + "from": "li02", + "to": "li04", + "type": "neutral" + }, + { + "id": "c-li02-li06", + "from": "li02", + "to": "li06", + "type": "standard" + }, + { + "id": "c-li06-li07", + "from": "li06", + "to": "li07", + "type": "neutral" + }, + { + "id": "c-rh01-rh02", + "from": "rh01", + "to": "rh02", + "type": "standard" + }, + { + "id": "c-rh01-rh03", + "from": "rh01", + "to": "rh03", + "type": "standard" + }, + { + "id": "c-rh01-rh04", + "from": "rh01", + "to": "rh04", + "type": "standard" + }, + { + "id": "c-rh01-rh05", + "from": "rh01", + "to": "rh05", + "type": "standard" + }, + { + "id": "c-rh02-rh04", + "from": "rh02", + "to": "rh04", + "type": "neutral" + }, + { + "id": "c-rh03-rh05", + "from": "rh03", + "to": "rh05", + "type": "neutral" + }, + { + "id": "c-rh04-rh05", + "from": "rh04", + "to": "rh05", + "type": "neutral" + }, + { + "id": "c-st01-st02", + "from": "st01", + "to": "st02", + "type": "neutral" + }, + { + "id": "c-st01-st03", + "from": "st01", + "to": "st03", + "type": "standard" + }, + { + "id": "c-st02-st02c", + "from": "st02", + "to": "st02c", + "type": "standard" + }, + { + "id": "c-st03-st03b", + "from": "st03", + "to": "st03b", + "type": "standard" + } + ] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..211ee1b --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "editable-star-map", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "import:universe": "node build-universe-map.mjs", + "start": "node server.mjs", + "test": "node --test star-map.test.mjs server.test.mjs" + }, + "engines": { + "node": ">=20" + } +} diff --git a/server.mjs b/server.mjs new file mode 100644 index 0000000..7ebbb53 --- /dev/null +++ b/server.mjs @@ -0,0 +1,145 @@ +import { createServer as createHttpServer } from "node:http"; +import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const projectDir = dirname(fileURLToPath(import.meta.url)); + +function sendJson(response, statusCode, value) { + response.writeHead(statusCode, { + "content-type": "application/json; charset=utf-8", + "cache-control": "no-store" + }); + response.end(JSON.stringify(value)); +} + +function sendText(response, statusCode, text, contentType = "text/plain; charset=utf-8") { + response.writeHead(statusCode, { + "content-type": contentType, + "cache-control": "no-store" + }); + response.end(text); +} + +async function readRequestBody(request, limitBytes = 5 * 1024 * 1024) { + const chunks = []; + let size = 0; + + for await (const chunk of request) { + size += chunk.length; + if (size > limitBytes) { + throw new Error("Payload zu gross"); + } + chunks.push(chunk); + } + + return Buffer.concat(chunks).toString("utf8"); +} + +function validateMapData(value) { + if (!value || typeof value !== "object") { + throw new Error("Karte muss ein Objekt sein"); + } + if (!Array.isArray(value.systems) || !Array.isArray(value.connections)) { + throw new Error("systems und connections muessen Arrays sein"); + } + + const ids = new Set(); + for (const system of value.systems) { + if (!system || typeof system !== "object") { + throw new Error("Ungueltiges System"); + } + if (typeof system.id !== "string" || typeof system.name !== "string") { + throw new Error("System braucht id und name"); + } + if (!Number.isFinite(system.x) || !Number.isFinite(system.y)) { + throw new Error("System braucht x/y Koordinaten"); + } + ids.add(system.id); + } + + for (const connection of value.connections) { + if (!connection || typeof connection !== "object") { + throw new Error("Ungueltige Verbindung"); + } + if (typeof connection.id !== "string" || typeof connection.from !== "string" || typeof connection.to !== "string") { + throw new Error("Verbindung braucht id/from/to"); + } + if (!ids.has(connection.from) || !ids.has(connection.to)) { + throw new Error("Verbindung verweist auf unbekanntes System"); + } + } + + return { + source: typeof value.source === "string" ? value.source : undefined, + background: typeof value.background === "string" ? value.background : "#05080f", + grid: value.grid && typeof value.grid === "object" ? value.grid : undefined, + viewBox: value.viewBox && typeof value.viewBox === "object" ? value.viewBox : undefined, + systems: value.systems, + connections: value.connections + }; +} + +async function readInitialMap(htmlPath) { + const html = await readFile(htmlPath, "utf8"); + const match = html.match(/ + + + + diff --git a/star-map.test.mjs b/star-map.test.mjs new file mode 100644 index 0000000..a8e3774 --- /dev/null +++ b/star-map.test.mjs @@ -0,0 +1,90 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { buildUniverseMap } from "./build-universe-map.mjs"; + +const html = readFileSync(new URL("./star-map.html", import.meta.url), "utf8"); +const startScript = readFileSync(new URL("./start-webserver.cmd", import.meta.url), "utf8"); + +function extractJsonScript(id) { + const pattern = new RegExp(``); + const match = html.match(pattern); + assert.ok(match, `missing JSON script #${id}`); + return JSON.parse(match[1]); +} + +test("universe import builds a grid map from universe.ini", async () => { + const data = await buildUniverseMap({ + universeDir: "C:/PROJECTS/REM/REM-Mod/DATA/UNIVERSE" + }); + + assert.equal(data.source, "universe.ini"); + assert.equal(data.systems.length, 58); + assert.ok(data.connections.length >= 80, "expected direct jump connections from system INIs"); + assert.deepEqual(data.grid, { + minCol: 0, + minRow: 0, + maxCol: 16, + maxRow: 16, + cellSize: 72, + margin: 72 + }); + + const newYork = data.systems.find((system) => system.id === "li01"); + assert.ok(newYork, "missing Li01"); + assert.equal(newYork.name, "New York"); + assert.equal(newYork.gridX, 7); + assert.equal(newYork.gridY, 8); + assert.equal(newYork.x, 576); + assert.equal(newYork.y, 648); + + assert.ok(data.connections.some((connection) => connection.from === "li01" && connection.to === "li02")); +}); + +test("template contains the systems and connections from universe.ini", () => { + const data = extractJsonScript("initial-map-data"); + + assert.equal(data.source, "universe.ini"); + assert.equal(data.systems.length, 58, "expected active systems from universe.ini"); + assert.ok(data.connections.length >= 80, "expected direct route set from system INIs"); + assert.ok(data.grid, "expected grid metadata"); + + for (const name of ["Tau-37", "Leeds", "New Tokyo", "Sigma-19", "New Berlin", "Omicron Prime"]) { + assert.ok(data.systems.some((system) => system.name === name), `missing ${name}`); + } + + for (const connection of data.connections) { + assert.ok(data.systems.some((system) => system.id === connection.from), `missing source ${connection.from}`); + assert.ok(data.systems.some((system) => system.id === connection.to), `missing target ${connection.to}`); + } +}); + +test("editor UI exposes creation, deletion, linking, notes, and background controls", () => { + assert.match(html, /data-mode="add"/, "missing add-system mode"); + assert.match(html, /data-mode="connect"/, "missing connection mode"); + assert.match(html, /id="deleteSelected"/, "missing delete action"); + assert.match(html, /id="systemNote"/, "missing per-system note field"); + assert.match(html, /id="backgroundColor"/, "missing configurable background"); + assert.match(html, /id="saveRemote"/, "missing persistent save action"); + assert.match(html, /id="loadRemote"/, "missing persistent load action"); + assert.doesNotMatch(html, /id="snapToGrid"/, "grid snapping must not be optional"); + assert.match(html, /function snapPointToGrid/, "missing grid snapping behavior"); + assert.doesNotMatch(html, /snapToGrid\.checked/, "grid snapping must always be active"); + assert.match(html, /function createSystem/, "missing system creation behavior"); + assert.match(html, /function createConnection/, "missing connection creation behavior"); + assert.match(html, /async function saveRemoteMap/, "missing server save behavior"); + assert.match(html, /async function loadRemoteMap/, "missing server load behavior"); +}); + +test("system dragging has a forgiving hit target and stable pointer handlers", () => { + assert.match(html, /class", "system-hit"/, "missing large system drag hit target"); + assert.match(html, /function handleDragMove/, "missing shared drag move handler"); + assert.match(html, /window\.addEventListener\("pointermove", handleDragMove\)/, "missing window pointermove handler"); + assert.match(html, /window\.addEventListener\("pointerup", handleDragEnd\)/, "missing window pointerup handler"); +}); + +test("start script launches the local server and opens the browser", () => { + assert.match(startScript, /node.*server\.mjs/i, "start script must launch server.mjs with node"); + assert.match(startScript, /127\.0\.0\.1:3000/i, "start script must open the local app URL"); + assert.match(startScript, /Start-Process/i, "start script must open the browser"); +}); diff --git a/start-webserver.cmd b/start-webserver.cmd new file mode 100644 index 0000000..cff902b --- /dev/null +++ b/start-webserver.cmd @@ -0,0 +1,27 @@ +@echo off +setlocal + +set "APP_DIR=%~dp0" +set "PORT=3000" +set "APP_URL=http://127.0.0.1:3000/" + +powershell -NoProfile -ExecutionPolicy Bypass -Command ^ + "$ErrorActionPreference = 'Stop';" ^ + "$appDir = $env:APP_DIR;" ^ + "$port = [int]$env:PORT;" ^ + "$url = $env:APP_URL;" ^ + "$listener = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue;" ^ + "if (-not $listener) {" ^ + " Start-Process -FilePath 'node' -ArgumentList 'server.mjs' -WorkingDirectory $appDir -WindowStyle Hidden;" ^ + " Start-Sleep -Milliseconds 900;" ^ + "}" ^ + "Start-Process $url;" + +if errorlevel 1 ( + echo Fehler: Server konnte nicht gestartet oder Browser nicht geoeffnet werden. + echo Pruefe, ob Node.js installiert ist und Port %PORT% frei ist. + pause + exit /b 1 +) + +endlocal