Files
REM-Planning/build-universe-map.mjs
2026-06-18 10:27:16 +02:00

258 lines
7.1 KiB
JavaScript

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(
/<script type="application\/json" id="initial-map-data">[\s\S]*?<\/script>/,
`<script type="application/json" id="initial-map-data">\n${json}\n </script>`
);
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`);
}