From 03b762e239a759de47d01eae9cf40c89bf746fb7 Mon Sep 17 00:00:00 2001 From: plenarius Date: Mon, 20 Apr 2026 19:12:03 -0400 Subject: [PATCH] feat: add 2DA intellisense parser and lookup for NWScript editor --- packages/backend/src/nwscript/twoda-index.ts | 80 ++++++++++++++++++++ packages/backend/src/routes/editor.ts | 19 +++++ 2 files changed, 99 insertions(+) create mode 100644 packages/backend/src/nwscript/twoda-index.ts diff --git a/packages/backend/src/nwscript/twoda-index.ts b/packages/backend/src/nwscript/twoda-index.ts new file mode 100644 index 0000000..2d9206d --- /dev/null +++ b/packages/backend/src/nwscript/twoda-index.ts @@ -0,0 +1,80 @@ +import fs from "fs/promises"; +import path from "path"; + +interface TwoDAFile { + columns: string[]; + rows: Map>; +} + +const files = new Map(); + +export async function loadTwoDAFiles(twodaDir: string): Promise { + files.clear(); + let entries; + try { + entries = await fs.readdir(twodaDir); + } catch { + return; // directory doesn't exist yet + } + + for (const filename of entries) { + if (!filename.endsWith(".2da")) continue; + try { + const content = await fs.readFile(path.join(twodaDir, filename), "utf-8"); + const parsed = parse2DA(content); + if (parsed) { + files.set(filename.replace(".2da", ""), parsed); + } + } catch { + // skip unparseable files + } + } +} + +function parse2DA(content: string): TwoDAFile | null { + const lines = content.split(/\r?\n/).filter(l => l.trim()); + if (lines.length < 3) return null; + + let headerLineIdx = -1; + for (let i = 0; i < lines.length; i++) { + if (lines[i].trim().startsWith("2DA")) continue; + if (lines[i].trim() === "") continue; + headerLineIdx = i; + break; + } + if (headerLineIdx === -1) return null; + + const columns = lines[headerLineIdx].trim().split(/\s+/); + const rows = new Map>(); + + for (let i = headerLineIdx + 1; i < lines.length; i++) { + const parts = lines[i].trim().split(/\s+/); + if (parts.length < 2) continue; + const rowIndex = parseInt(parts[0], 10); + if (isNaN(rowIndex)) continue; + const rowData = new Map(); + for (let c = 0; c < columns.length; c++) { + const val = parts[c + 1] || "****"; + rowData.set(columns[c], val); + } + rows.set(rowIndex, rowData); + } + + return { columns, rows }; +} + +export function get2DAFile(name: string): TwoDAFile | undefined { + return files.get(name.toLowerCase()); +} + +export function list2DAFiles(): string[] { + return Array.from(files.keys()); +} + +export function get2DARow(name: string, row: number): Record | undefined { + const file = files.get(name.toLowerCase()); + if (!file) return undefined; + const rowData = file.rows.get(row); + if (!rowData) return undefined; + return Object.fromEntries(rowData); +} diff --git a/packages/backend/src/routes/editor.ts b/packages/backend/src/routes/editor.ts index 30a4eb3..73e05d8 100644 --- a/packages/backend/src/routes/editor.ts +++ b/packages/backend/src/routes/editor.ts @@ -7,6 +7,7 @@ import { } from "../services/editor.service.js"; import { lookupResref, getResrefCount } from "../nwscript/resref-index.js"; import { lookupTlk, getTlkCount } from "../nwscript/tlk-index.js"; +import { get2DAFile, list2DAFiles, get2DARow } from "../nwscript/twoda-index.js"; const router = Router(); @@ -78,4 +79,22 @@ router.get("/tlk-count", (_req, res) => { res.json({ count: getTlkCount() }); }); +router.get("/2da", (_req, res) => { + res.json({ files: list2DAFiles() }); +}); + +router.get("/2da/:name", (req, res) => { + const file = get2DAFile(req.params.name); + if (!file) return res.status(404).json({ error: "2DA file not found" }); + res.json({ columns: file.columns, rowCount: file.rows.size }); +}); + +router.get("/2da/:name/:row", (req, res) => { + const row = parseInt(req.params.row, 10); + if (isNaN(row)) return res.status(400).json({ error: "invalid row" }); + const data = get2DARow(req.params.name, row); + if (!data) return res.status(404).json({ error: "row not found" }); + res.json(data); +}); + export default router;