import https from "node:https"; import fs from "fs"; import handlebars from "handlebars"; import type * as M from "./factorio-api.models.d.ts"; interface Data { app: string; appVersion: string; apiVersion: number; stage: string; interfaces: Interface[]; types: DataType[]; } interface Interface { name: string; description: string; export: boolean; extends?: string; typeName?: string; props: Property[]; isTypeFunction?: string; } interface Property { name: string; description: string; optional: boolean; type: string; } interface DataType { name: string; description: string; value: string; } const API_PATH = "https://lua-api.factorio.com/latest/prototype-api.json"; function getPrototypeApi(): Promise { return new Promise((resolve, reject) => { https .get(API_PATH, (res) => { let data = ""; res.on("data", (chunk: string) => { data += chunk; }); res.on("end", () => { const api = JSON.parse(data) as M.PrototypeApi; resolve(api); }); }) .on("error", (err) => { reject(err); }); }); } function parseType(type: M.DataType, structName?: string): string { if (typeof type === "string") { // Handle known built-in types switch (type) { case "bool": return "boolean"; case "double": case "float": case "int8": case "int16": case "int32": case "uint8": case "uint16": case "uint32": case "uint64": return "number"; case "string": return "string"; case "DataExtendMethod": return "() => void"; default: return type; } } switch (type.complex_type) { case "array": return `${parseType(type.value, structName)}[]`; case "dictionary": return `Record<${parseType(type.key, structName)}, ${parseType(type.value, structName)}>`; case "literal": { if (typeof type.value === "string") { return `'${type.value}'`; } else { return type.value.toString(); } } case "struct": if (structName == null) throw new Error( "Unexpected struct: use properties on parent or pass a struct name to use" ); return structName; case "tuple": return `[${type.values.map((v) => parseType(v, structName)).join(", ")}]`; case "type": return parseType(type.value, structName); case "union": return `(${type.options.map((o) => parseType(o, structName)).join(" | ")})`; } } function parseProperty(prop: M.Property): Property { return { name: prop.name, description: prop.description, optional: prop.optional, type: parseType(prop.type), }; } function getTypePropertyValue(props: M.Property[]): string | undefined { const typeProp = props.find((p) => p.name === "type"); if ( typeProp && typeof typeProp.type !== "string" && typeProp.type.complex_type === "literal" && typeof typeProp.type.value === "string" ) { return typeProp.type.value; } return undefined; } function parsePrototypeApi(api: M.PrototypeApi): Data { const data: Data = { app: api.application, appVersion: api.application_version, apiVersion: api.api_version, stage: api.stage, interfaces: [], types: [], }; api.prototypes.forEach((p) => { const int: Interface = { name: p.name, description: p.description, export: p.parent == null, extends: p.parent, props: p.properties.map((p) => parseProperty(p)), }; const typePropValue = getTypePropertyValue(p.properties); if (typePropValue != null) { int.isTypeFunction = typePropValue; } else if (p.typename) { int.typeName = p.typename; int.isTypeFunction = p.typename; } data.interfaces.push(int); }); api.types.forEach((t) => { if (typeof t.type === "string") { if (t.type !== "builtin") { const dataType: DataType = { name: t.name, description: t.description, value: parseType(t.type), }; data.types.push(dataType); } } else if (t.type.complex_type === "struct") { const int: Interface = { name: t.name, description: t.description, export: t.parent == null, extends: t.parent, props: [], }; if (t.properties) { int.props = t.properties.map((p) => parseProperty(p)); int.isTypeFunction = getTypePropertyValue(t.properties); } data.interfaces.push(int); } else { let structName: string | undefined; if (t.properties) { structName = `_${t.name}`; const int: Interface = { name: structName, description: t.description, export: false, extends: t.parent, props: t.properties.map((p) => parseProperty(p)), }; data.interfaces.push(int); } const dataType: DataType = { name: t.name, description: t.description, value: parseType(t.type, structName), }; data.types.push(dataType); } }); return data; } const generate = async function (): Promise { console.log(`Regenerating Factorio prototype models from ${API_PATH}...`); const api = await getPrototypeApi(); const data = parsePrototypeApi(api); const modelsSource = `/** Generated file, do not edit. See scripts/factorio-api.ts for generator. */ /** * Application: {{app}} * Version: {{appVersion}} * API Version: {{apiVersion}} */ {{#interfaces}} {{#if description}} /** {{{description}}} */ {{/if}} {{#unless props.length}} // eslint-disable-next-line @typescript-eslint/no-empty-interface {{/unless}} {{#if export}}export {{/if}}interface {{#if extends}}_{{/if}}{{name}} { {{#if typeName}} type: '{{typeName}}'; {{/if}} {{#props}} {{#if description}} /** {{{description}}} */ {{/if}} {{name}}{{#if optional}}?{{/if}}: {{{type}}}; {{/props}} } {{#if extends}} export type {{name}} = _{{name}} & Omit<{{extends}}, keyof _{{name}}>; {{/if}} {{#if isTypeFunction}} export function is{{name}}(value: unknown): value is {{name}} { return (value as { type: string }).type === '{{isTypeFunction}}'; } {{/if}} {{/interfaces}} {{#types}} {{#if description}} /** {{{description}}} */ {{/if}} export type {{name}} = {{{value}}}; {{/types}}`; const modelsTemplate = handlebars.compile(modelsSource); const result = modelsTemplate(data); fs.writeFileSync("scripts/factorio-dump.models.ts", result); console.log(`Generated ${api.prototypes.length.toString()} models`); }; void generate();