181 lines
7.2 KiB
TypeScript
181 lines
7.2 KiB
TypeScript
import { readdir, writeFile, readFile } from 'fs/promises';
|
|
import { existsSync } from 'fs';
|
|
import { join, resolve } from 'path';
|
|
import os from 'os';
|
|
import AdmZip from 'adm-zip';
|
|
|
|
// --- Constants ---
|
|
const GERMAN_LOCALE_DIR = 'en';
|
|
const LOCALE_DIR = 'locale';
|
|
const BASE_LOCALE_FILENAME = 'base.cfg';
|
|
const OUTPUT_CSV_FILENAME = 'factorio_english_items.csv';
|
|
const LOCALE_TARGET_SECTIONS = new Set([
|
|
'entity-name',
|
|
'item-name',
|
|
'fluid-name',
|
|
'equipment-name',
|
|
'virtual-signal-name',
|
|
]);
|
|
const LOCALE_KEY_VALUE_DELIMITER = '=';
|
|
const LOCALE_COMMENT_PREFIXES = ['#', ';'];
|
|
const MOD_FILE_EXTENSION = '.zip';
|
|
const CSV_HEADER = 'section,item_key,localized_name\n';
|
|
const DEFAULT_ENCODING: BufferEncoding = 'utf8';
|
|
const LOCALE_FILE_REGEX = new RegExp(`[\\\\/]?${LOCALE_DIR}[\\\\/]${GERMAN_LOCALE_DIR}[\\\\/].*\\.cfg$`, 'i');
|
|
|
|
/**
|
|
* A list of common Factorio installation directories checked in order.
|
|
* Add non-standard installation paths here if needed.
|
|
*/
|
|
const FACTORIO_INSTALL_PATHS: string[] = [
|
|
'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Factorio',
|
|
'C:\\Program Files\\Steam\\steamapps\\common\\Factorio',
|
|
'C:\\Program Files\\Factorio',
|
|
];
|
|
|
|
const FACTORIO_USERDATA_PATHS: Record<string, string> = {
|
|
win32: join(process.env.APPDATA ?? '', 'Factorio'),
|
|
darwin: join(os.homedir(), 'Library', 'Application Support', 'factorio'),
|
|
linux: join(os.homedir(), '.factorio'),
|
|
};
|
|
|
|
/**
|
|
* Resolves the Factorio installation and user data directories for the current platform.
|
|
* @returns An object with `installPath` and `userdataPath`, each null if not found.
|
|
*/
|
|
function getFactorioPaths(): { installPath: string | null; userdataPath: string | null } {
|
|
const installPath = FACTORIO_INSTALL_PATHS.find(existsSync) ?? null;
|
|
const userdataPath = FACTORIO_USERDATA_PATHS[os.platform()] ?? null;
|
|
return {
|
|
installPath,
|
|
userdataPath: userdataPath && existsSync(userdataPath) ? userdataPath : null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parses INI/CFG file content to extract key-value pairs from all target sections.
|
|
* @param content The raw text content of the .cfg file.
|
|
* @returns A map of `"section/key"` to `{ section, key, value }`.
|
|
*/
|
|
function parseLocale(content: string): Map<string, { section: string; key: string; value: string }> {
|
|
const items = new Map<string, { section: string; key: string; value: string }>();
|
|
const lines = content.split(/\r?\n/);
|
|
let currentSection: string | null = null;
|
|
|
|
for (const line of lines) {
|
|
const trimmedLine = line.trim();
|
|
if (trimmedLine.startsWith('[') && trimmedLine.endsWith(']')) {
|
|
const sectionName = trimmedLine.slice(1, -1);
|
|
currentSection = LOCALE_TARGET_SECTIONS.has(sectionName) ? sectionName : null;
|
|
continue;
|
|
}
|
|
|
|
const isComment = LOCALE_COMMENT_PREFIXES.some(prefix => trimmedLine.startsWith(prefix));
|
|
if (currentSection && trimmedLine && !isComment) {
|
|
const parts = trimmedLine.split(LOCALE_KEY_VALUE_DELIMITER, 2);
|
|
if (parts.length === 2) {
|
|
const [key, value] = parts;
|
|
if (key && value) {
|
|
const compositeKey = `${currentSection}/${key.trim()}`;
|
|
if (!items.has(compositeKey)) {
|
|
items.set(compositeKey, { section: currentSection, key: key.trim(), value: value.trim() });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
/**
|
|
* Escapes a string for use in a CSV field, quoting where necessary.
|
|
* @param value The value to escape.
|
|
* @returns The CSV-safe value.
|
|
*/
|
|
function toCsvField(value: string): string {
|
|
if (/[",\n]/.test(value)) {
|
|
return `"${value.replace(/"/g, '""')}"`;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Main function to orchestrate the scanning, parsing, and exporting process.
|
|
*/
|
|
async function run(): Promise<void> {
|
|
const allItems = new Map<string, { section: string; key: string; value: string }>();
|
|
const { installPath, userdataPath } = getFactorioPaths();
|
|
|
|
const mergeItems = (source: Map<string, { section: string; key: string; value: string }>) => {
|
|
for (const [compositeKey, entry] of source) {
|
|
if (!allItems.has(compositeKey)) allItems.set(compositeKey, entry);
|
|
}
|
|
};
|
|
|
|
// --- Process Base Game File First ---
|
|
if (installPath) {
|
|
const baseLocalePath = join(installPath, 'data', 'base', LOCALE_DIR, GERMAN_LOCALE_DIR, BASE_LOCALE_FILENAME);
|
|
console.log(`Attempting to read base game locale from: ${baseLocalePath}`);
|
|
if (existsSync(baseLocalePath)) {
|
|
try {
|
|
const content = await readFile(baseLocalePath, DEFAULT_ENCODING);
|
|
console.log(` -> Reading ${baseLocalePath}`);
|
|
mergeItems(parseLocale(content));
|
|
} catch (err) {
|
|
console.warn(`[WARN] Could not read or parse base game locale file.`, err);
|
|
}
|
|
} else {
|
|
console.warn(`[WARN] Base game locale file not found at the expected path. Skipping.`);
|
|
}
|
|
} else {
|
|
console.warn(`[WARN] Factorio installation not found. Searched paths defined in FACTORIO_INSTALL_PATHS. Skipping base game file.`);
|
|
}
|
|
|
|
// --- Process Mods ---
|
|
if (userdataPath) {
|
|
const modsPath = join(userdataPath, 'mods');
|
|
console.log(`\nScanning for mods in: ${modsPath}`);
|
|
if (!existsSync(modsPath)) {
|
|
console.error(`[ERROR] Mods directory not found at the expected user data path.`);
|
|
} else {
|
|
const files = await readdir(modsPath);
|
|
const zipFiles = files.filter(file => file.toLowerCase().endsWith(MOD_FILE_EXTENSION));
|
|
console.log(`Found ${zipFiles.length} zip archives to process.`);
|
|
|
|
for (const zipFile of zipFiles) {
|
|
const zipPath = join(modsPath, zipFile);
|
|
try {
|
|
const zip = new AdmZip(zipPath);
|
|
for (const entry of zip.getEntries()) {
|
|
if (LOCALE_FILE_REGEX.test(entry.entryName)) {
|
|
console.log(` -> Reading ${entry.entryName} from ${zipFile}`);
|
|
const content = entry.getData().toString(DEFAULT_ENCODING);
|
|
mergeItems(parseLocale(content));
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn(`[WARN] Could not process ${zipFile}. It may be corrupted or unreadable.`, err);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
console.warn(`[WARN] Factorio user data directory not found. Skipping mod processing.`);
|
|
}
|
|
|
|
if (allItems.size === 0) {
|
|
console.log('\nFinished. No entries were found for the target sections.');
|
|
return;
|
|
}
|
|
|
|
console.log(`\nFound a total of ${allItems.size} unique entries across all files.`);
|
|
const csvRows = Array.from(allItems.values()).map(({ section, key, value }) =>
|
|
`${toCsvField(section)},${toCsvField(key)},${toCsvField(value)}`
|
|
);
|
|
const outputCsvPath = resolve(process.cwd(), OUTPUT_CSV_FILENAME);
|
|
await writeFile(outputCsvPath, CSV_HEADER + csvRows.join('\n'), DEFAULT_ENCODING);
|
|
console.log(`Successfully exported all entries to: ${outputCsvPath}`);
|
|
}
|
|
|
|
run().catch(err => {
|
|
console.error("\n[FATAL] An unexpected error occurred:", err);
|
|
}); |