e0a118f52b
Pulls items/perks/buffs/skills CSVs and command docs from pryans/kcd2-cheat, parses the mod's CSV dialect (backslash-escaped commas, multiline quoted fields) and BBCode command docs into vendored JSON under src/data/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
182 lines
6.4 KiB
JavaScript
182 lines
6.4 KiB
JavaScript
// Regenerates src/data/*.json from the KCD2 "Cheat" mod repository
|
|
// (https://github.com/pryans/kcd2-cheat — source of the Nexus "Cheat" mod).
|
|
//
|
|
// Usage: node scripts/fetch-data.mjs
|
|
//
|
|
// Output is committed to the repo so builds are offline; re-run to refresh
|
|
// when the mod updates its item/perk/buff/skill dumps or command docs.
|
|
|
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const BASE = 'https://raw.githubusercontent.com/pryans/kcd2-cheat/main/docs/';
|
|
const OUT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'src', 'data');
|
|
|
|
async function fetchText(name) {
|
|
const res = await fetch(BASE + name);
|
|
if (!res.ok) throw new Error(`Fetching ${name} failed: HTTP ${res.status}`);
|
|
return (await res.text()).replaceAll('\r\n', '\n');
|
|
}
|
|
|
|
// The mod's CSV dialect: fields are double-quoted (header and empty trailing
|
|
// fields are bare), commas inside fields are backslash-escaped, and quoted
|
|
// fields may contain literal newlines.
|
|
function parseCsv(text) {
|
|
const rows = [];
|
|
let row = [];
|
|
let field = '';
|
|
let inQuotes = false;
|
|
for (let i = 0; i < text.length; i++) {
|
|
const c = text[i];
|
|
if (inQuotes) {
|
|
if (c === '\\' && i + 1 < text.length) {
|
|
field += text[++i];
|
|
} else if (c === '"') {
|
|
inQuotes = false;
|
|
} else {
|
|
field += c;
|
|
}
|
|
} else if (c === '"') {
|
|
inQuotes = true;
|
|
} else if (c === ',') {
|
|
row.push(field);
|
|
field = '';
|
|
} else if (c === '\n') {
|
|
row.push(field);
|
|
if (row.some((f) => f !== '')) rows.push(row);
|
|
row = [];
|
|
field = '';
|
|
} else {
|
|
field += c;
|
|
}
|
|
}
|
|
row.push(field);
|
|
if (row.some((f) => f !== '')) rows.push(row);
|
|
return rows.slice(1); // drop header
|
|
}
|
|
|
|
// Skill/perk descriptions are HTML-entity-encoded rich text; flatten to plain
|
|
// text with paragraph breaks.
|
|
function cleanText(s) {
|
|
return s
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll(''', "'")
|
|
.replaceAll(''', "'")
|
|
.replaceAll('&', '&')
|
|
.replace(/<\/p>\s*<p>/g, '\n\n')
|
|
.replace(/<br\s*\/?>/g, '\n')
|
|
.replace(/<[^>]+>/g, '')
|
|
.trim();
|
|
}
|
|
|
|
const CATEGORY_RULES = [
|
|
[/money/, 'Money'],
|
|
[/item|inventory|stash|gear|stolen/, 'Items & Inventory'],
|
|
[/skill|stat/, 'Skills & Stats'],
|
|
[/perk/, 'Perks'],
|
|
[/buff|potion/, 'Buffs'],
|
|
[/horse/, 'Horse'],
|
|
[/teleport|_loc$|clip|phys_|checkpoint/, 'Movement & Teleport'],
|
|
[/npc|kill|spawn|charm|target|revive/, 'NPCs'],
|
|
[/time|weather|map/, 'World & Time'],
|
|
[/lockpick|pickpocket/, 'Minigames'],
|
|
[/hud|compass|statusbar|reticle|third_person|regen|wash|save|state/, 'Player & UI'],
|
|
[/action|alias|localization|find_/, 'Utility'],
|
|
];
|
|
|
|
function categorize(name) {
|
|
for (const [re, cat] of CATEGORY_RULES) if (re.test(name)) return cat;
|
|
return 'Misc';
|
|
}
|
|
|
|
function stripBbcode(s) {
|
|
return s.replace(/\[\/?(?:b|i|u|size|color|url)(?:=[^\]]*)?\]/g, '').trim();
|
|
}
|
|
|
|
// docs.txt: intro + command index, then one block per command:
|
|
// [size=4][b]name[/b][/size]
|
|
// description lines
|
|
// [b]Arguments:[/b] (optional section, tab-indented "arg: (required type) desc")
|
|
// [b]Examples:[/b] (optional section, tab-indented caption/command groups)
|
|
function parseCommandDocs(text) {
|
|
const version = text.match(/Cheat version ([\d.]+)/)?.[1] ?? null;
|
|
const commands = [];
|
|
const blockRe = /^\[size=4\]\[b\](\w+)\[\/b\]\[\/size\]$/gm;
|
|
const headers = [...text.matchAll(blockRe)];
|
|
for (let h = 0; h < headers.length; h++) {
|
|
const name = headers[h][1];
|
|
const start = headers[h].index + headers[h][0].length;
|
|
const end = h + 1 < headers.length ? headers[h + 1].index : text.length;
|
|
const lines = text.slice(start, end).split('\n');
|
|
|
|
let section = 'desc';
|
|
const descLines = [];
|
|
const args = [];
|
|
const examples = [];
|
|
let example = null;
|
|
for (const rawLine of lines) {
|
|
const line = rawLine.replace(/\s+$/, '');
|
|
if (/^\[b\]Arguments:\[\/b\]$/.test(line)) { section = 'args'; continue; }
|
|
if (/^\[b\]Examples:\[\/b\]$/.test(line)) { section = 'examples'; continue; }
|
|
if (section === 'desc') {
|
|
descLines.push(stripBbcode(line));
|
|
} else if (section === 'args') {
|
|
const m = line.match(/^\t(\w+): \((required|optional)(?: (\w+))?\)\s*(.*)$/);
|
|
if (m) args.push({ name: m[1], required: m[2] === 'required', type: m[3] ?? 'string', desc: m[4] });
|
|
} else if (section === 'examples') {
|
|
const t = line.replace(/^\t/, '');
|
|
if (t === '') {
|
|
if (example?.command) { examples.push(example); example = null; }
|
|
} else if (t.startsWith(name)) {
|
|
example ??= { caption: '' };
|
|
example.command = t;
|
|
} else {
|
|
example ??= { caption: '' };
|
|
example.caption = (example.caption ? example.caption + ' ' : '') + t.replace(/:$/, '');
|
|
}
|
|
}
|
|
}
|
|
if (example?.command) examples.push(example);
|
|
commands.push({
|
|
name,
|
|
desc: descLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(),
|
|
args,
|
|
examples,
|
|
category: categorize(name),
|
|
source: 'cheatmod',
|
|
});
|
|
}
|
|
return { version, commands };
|
|
}
|
|
|
|
async function writeJson(name, data) {
|
|
await writeFile(join(OUT_DIR, name), JSON.stringify(data));
|
|
console.log(`${name}: ${Array.isArray(data) ? data.length : data.commands?.length ?? ''} records`);
|
|
}
|
|
|
|
await mkdir(OUT_DIR, { recursive: true });
|
|
|
|
const [itemsCsv, perksCsv, buffsCsv, skillsCsv, docsTxt] = await Promise.all(
|
|
['items.csv', 'perks.csv', 'buffs.csv', 'skills.csv', 'docs.txt'].map(fetchText),
|
|
);
|
|
|
|
const items = parseCsv(itemsCsv).map(([id, name, desc]) => ({ id, name, desc: cleanText(desc ?? '') }));
|
|
const perks = parseCsv(perksCsv).map(([id, name, desc]) => ({ id, name, desc: cleanText(desc ?? '') }));
|
|
const buffs = parseCsv(buffsCsv).map(([id, name, params, desc]) => ({ id, name, params: params ?? '', desc: cleanText(desc ?? '') }));
|
|
const skills = parseCsv(skillsCsv).map(([id, name, desc]) => ({ id, name, desc: cleanText(desc ?? '') }));
|
|
const { version, commands } = parseCommandDocs(docsTxt);
|
|
|
|
await writeJson('items.json', items);
|
|
await writeJson('perks.json', perks);
|
|
await writeJson('buffs.json', buffs);
|
|
await writeJson('skills.json', skills);
|
|
await writeJson('commands-mod.json', commands);
|
|
await writeJson('meta.json', {
|
|
source: 'https://github.com/pryans/kcd2-cheat',
|
|
cheatModDocsVersion: version,
|
|
fetchedAt: new Date().toISOString(),
|
|
});
|