feat: add 2DA intellisense parser and lookup for NWScript editor

This commit is contained in:
plenarius
2026-04-20 19:12:03 -04:00
parent 99d0c4f1e9
commit 03b762e239
2 changed files with 99 additions and 0 deletions
@@ -0,0 +1,80 @@
import fs from "fs/promises";
import path from "path";
interface TwoDAFile {
columns: string[];
rows: Map<number, Map<string, string>>;
}
const files = new Map<string, TwoDAFile>();
export async function loadTwoDAFiles(twodaDir: string): Promise<void> {
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<number, Map<string, string>>();
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<string, string>();
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<string, string> | 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);
}
+19
View File
@@ -7,6 +7,7 @@ import {
} from "../services/editor.service.js"; } from "../services/editor.service.js";
import { lookupResref, getResrefCount } from "../nwscript/resref-index.js"; import { lookupResref, getResrefCount } from "../nwscript/resref-index.js";
import { lookupTlk, getTlkCount } from "../nwscript/tlk-index.js"; import { lookupTlk, getTlkCount } from "../nwscript/tlk-index.js";
import { get2DAFile, list2DAFiles, get2DARow } from "../nwscript/twoda-index.js";
const router = Router(); const router = Router();
@@ -78,4 +79,22 @@ router.get("/tlk-count", (_req, res) => {
res.json({ count: getTlkCount() }); 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; export default router;