| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- import https from "node:https";
- import fs from "fs";
- import handlebars from "handlebars";
- import * as M from "./factorio-api.models.js";
- 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<M.PrototypeApi> {
- 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<void> {
- 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();
|