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`); }