Initial REM planning editor
This commit is contained in:
@@ -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(
|
||||
/<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`);
|
||||
}
|
||||
Reference in New Issue
Block a user