258 lines
7.1 KiB
JavaScript
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`);
|
|
}
|