Share structured data types in export

Place shared types of the export functionality into lib and use in the
web interface to make sure the data is not incorrectly handled.

Resolves #526
This commit is contained in:
Hornwitser committed 2026-03-12 14:45:48 +01:00
1 parent bcabad567e
commit 076fb933dc
9 files changed
+143 -35

No files matched your search

+1 -1
View File
@@ -246,7 +246,7 @@ async function uploadExport(req: Request, res: Response) {
];
let assets: any = {};
let settingPrototypes = {};
let settingPrototypes: lib.ExportSettings = {};
for (let filePath of exportFiles) {
let file = zip.file(filePath);
if (!file) {
+8 -15
View File
@@ -8,11 +8,6 @@ import * as lib from "@clusterio/lib";
import * as libBuildMod from "@clusterio/lib/dist/node/build_mod";
import type { FactorioServer } from "./server";
interface Prototype {
[index: string]: unknown,
type: string,
name: string,
}
interface SimpleIconSpecification {
icons?: never;
icon: string;
@@ -33,11 +28,9 @@ interface LayeredIconSpecification {
icon_mipmaps?: number;
}
type IconSpecification = LayeredIconSpecification | SimpleIconSpecification;
type ItemPrototype = Prototype & IconSpecification;
type ItemPrototype = lib.FactorioPrototypeBase & IconSpecification;
type Prototypes = Record<string, Record<string, Prototype>>
/**
* Generate the export mod needed for exportData
*
@@ -326,7 +319,7 @@ const entityTypes = new Set([
"agricultural-tower", "market", "lane-splitter",
]);
function filterPrototypes(prototypes: Prototypes, allowedTypes: Set<string>): ItemPrototype[] {
function filterPrototypes(prototypes: lib.FactorioPrototypes, allowedTypes: Set<string>): ItemPrototype[] {
return Object.entries(prototypes)
.filter(([type]) => allowedTypes.has(type))
.flatMap(([_, typePrototypes]) => Object.values(typePrototypes) as ItemPrototype[])
@@ -336,7 +329,7 @@ function filterPrototypes(prototypes: Prototypes, allowedTypes: Set<string>): It
/** Shared state for building a unified spritesheet across all categories. */
interface SheetState {
sheet: Jimp;
metadata: Map<string, { x: number, y: number, size: number, category: string, path?: string }>;
metadata: Map<string, lib.ExportMetadataEntry>;
iconCache: IconCache;
simpleIcons: Map<string, number>;
pos: number;
@@ -514,12 +507,12 @@ async function exportLocale(
export async function exportData(server: FactorioServer) {
await generateExportMod(server);
let settings: Prototypes = {};
let prototypes: Prototypes = {};
let settings: lib.ExportSettings = {};
let prototypes: lib.ExportPrototypes = {};
let modVersions = new Map();
let modOrder: string[] = [];
function add(obj: Prototypes, prototype: Prototype) {
function add(obj: lib.FactorioPrototypes, prototype: lib.FactorioPrototypeBase) {
if (!Object.prototype.hasOwnProperty.call(obj, prototype.type)) {
obj[prototype.type] = {};
}
@@ -598,7 +591,7 @@ export async function exportData(server: FactorioServer) {
let zip = new JSZip();
zip.file("export/settings.json", JSON.stringify(settings));
zip.file("export/prototypes.json", JSON.stringify(prototypes));
zip.file("export/locale.json", JSON.stringify([...locale.entries()]));
zip.file("export/locale.json", JSON.stringify([...locale.entries()] satisfies lib.ExportLocale));
// Build a single unified spritesheet across all prototype categories.
const state = await createSheetState();
@@ -616,7 +609,7 @@ export async function exportData(server: FactorioServer) {
const usedRows = Math.ceil(state.pos / (SHEET_WIDTH / SHEET_ICON_SIZE));
state.sheet.crop(0, 0, SHEET_WIDTH, Math.max(usedRows * SHEET_ICON_SIZE, 1));
zip.file("export/spritesheet.png", await state.sheet.getBufferAsync(Jimp.MIME_PNG));
zip.file("export/metadata.json", JSON.stringify([...state.metadata.entries()]));
zip.file("export/metadata.json", JSON.stringify([...state.metadata.entries()] satisfies lib.ExportMetadata));
server._logger.info(`Export complete: ${state.metadata.size} icons on ${usedRows} row(s)`);
+1
View File
@@ -2,6 +2,7 @@ export * from "./src/api";
export * from "./src/config";
export * from "./src/data";
export * from "./src/errors";
export * from "./src/export";
export * from "./src/factorio/exchange_string";
export * from "./src/helpers";
export * from "./src/ini";
+1
View File
@@ -9,6 +9,7 @@ export * from "./src/config";
export * from "./src/data";
export * from "./src/database";
export * from "./src/errors";
export * from "./src/export";
export * from "./src/external";
export * from "./src/factorio";
export * from "./src/file_ops";
+4 -3
View File
@@ -5,6 +5,7 @@ import * as libSchema from "../schema";
import * as libLuaTools from "../lua_tools";
import ExportManifest from "./ExportManifest";
import type { ExportSettings } from "../export";
import type { Logger } from "../logging";
import ModInfo, { ModDependencyUnsatisfiedReason } from "./ModInfo";
@@ -357,7 +358,7 @@ export default class ModPack {
* Setting prototypes exported from the game.
* @param logger - Logger used to report warnings on.
*/
fillDefaultSettings(settingPrototypes: Record<string, object>, logger: Logger) {
fillDefaultSettings(settingPrototypes: ExportSettings, logger: Logger) {
const knownTypes = ["bool-setting", "int-setting", "double-setting", "string-setting", "color-setting"];
let prototypes = Object.entries(settingPrototypes)
.filter(([type, _]) => knownTypes.includes(type))
@@ -370,7 +371,7 @@ export default class ModPack {
logger.warn(`Ignoring ${prototype.name} with unknown setting_type ${settingType}`);
continue;
}
if (prototype.default_value === undefined) {
if ((prototype as any).default_value === undefined) {
logger.warn(`Ignoring ${prototype.name} with missing default_value`);
continue;
}
@@ -389,7 +390,7 @@ export default class ModPack {
if (type === "color-setting") {
value = libLuaTools.normalizeColor(value as object);
}
this.settings[settingType].set(prototype.name, { value });
this.settings[settingType].set(prototype.name, { value } as ModSetting);
}
}
+119
View File
@@ -0,0 +1,119 @@
import { FactorioColor } from "./lua_tools";
/**
* A localised string.
* @see [LocalisedString](https://lua-api.factorio.com/stable/types/LocalisedString.html)
*/
export type FactorioLocalisedString = string | [string, ...FactorioLocalisedString[]];
/**
* Shared properties of all prototypes.
* @see [PrototypeBase](https://lua-api.factorio.com/stable/types/PrototypeBase.html)
*/
export interface FactorioPrototypeBase {
[index: string]: unknown,
type: string,
name: string,
order?: string,
localised_name?: FactorioLocalisedString,
localised_description?: FactorioLocalisedString,
factoriopedia_description?: FactorioLocalisedString,
subgroup?: string,
hidden?: boolean,
hidden_in_factoriopedia?: boolean,
parameter?: boolean,
factoriopedia_simulation?: unknown,
}
/**
* Stage the mod setting is part of.
* @see [settings_type property](https://wiki.factorio.com/Tutorial:Mod_settings#The_setting_type_property)
*/
export type FactorioModSettingType = "startup" | "runtime-global" | "runtime-per-user";
/**
* Boolean mod setting prototype.
* @see [bool-setting](https://wiki.factorio.com/Tutorial:Mod_settings#bool-setting)
*/
export interface FactorioBoolSettingPrototype extends FactorioPrototypeBase{
type: "bool-setting",
setting_type?: FactorioModSettingType,
default_value: boolean,
forced_value?: boolean,
}
/**
* Integer mod setting prototype.
* @see [int-setting](https://wiki.factorio.com/Tutorial:Mod_settings#int-setting)
*/
export interface FactorioIntSettingPrototype extends FactorioPrototypeBase{
type: "int-setting",
setting_type?: FactorioModSettingType,
default_value: number,
minimum_value?: number,
maximum_value?: number,
allowed_values?: number[],
}
/**
* Real number mod setting prototype.
* @see [double-setting](https://wiki.factorio.com/Tutorial:Mod_settings#double-setting)
*/
export interface FactorioDoubleSettingPrototype extends FactorioPrototypeBase{
type: "double-setting",
setting_type?: FactorioModSettingType,
default_value: number,
minimum_value?: number,
maximum_value?: number,
allowed_values?: number[],
}
/**
* String mod setting prototype.
* @see [string-setting](https://wiki.factorio.com/Tutorial:Mod_settings#string-setting)
*/
export interface FactorioStringSettingPrototype extends FactorioPrototypeBase{
type: "string-setting",
setting_type?: FactorioModSettingType,
default_value: string,
allow_black?: boolean,
auto_trim?: string,
allowed_values?: string[],
}
/**
* Color mod setting prototype.
* @see [color-setting](https://wiki.factorio.com/Tutorial:Mod_settings#color-setting)
*/
export interface FactorioColorSettingPrototype extends FactorioPrototypeBase{
type: "color-setting",
setting_type?: FactorioModSettingType,
default_value: FactorioColor,
forced_value?: boolean,
}
export type FactorioModSettingPrototype =
| FactorioBoolSettingPrototype
| FactorioIntSettingPrototype
| FactorioDoubleSettingPrototype
| FactorioStringSettingPrototype
| FactorioColorSettingPrototype
;
/**
* A list of prototypes as it appears in Factorio's Lua API.
* @see [Data.raw](https://lua-api.factorio.com/stable/types/Data.html#raw)
*/
export type FactorioPrototypes<Prototype = FactorioPrototypeBase> = Record<string, Record<string, Prototype>>
export type ExportSettings = FactorioPrototypes<FactorioModSettingPrototype>;
export type ExportPrototypes = FactorioPrototypes;
export type ExportLocale = [string, string][];
export interface ExportMetadataEntry {
x: number,
y: number,
size: number,
category: string,
path?: string,
}
export type ExportMetadata = [string, ExportMetadataEntry][];
+1
View File
@@ -10,6 +10,7 @@
"./src/check_singleton_import.ts",
"./src/datastore.ts",
"./src/errors.ts",
"./src/export.ts",
"./src/factorio/exchange_string.ts",
"./src/helpers.ts",
"./src/hash.ts",
+6 -14
View File
@@ -1,18 +1,10 @@
import { useEffect, useState } from "react";
import { useExportManifest } from "./export_manifest";
import { ExportMetadataEntry } from "@clusterio/lib";
type Metadata = {
x: number;
y: number;
size: number;
category: string;
localised_name?: any[];
path?: string;
};
const metadataCaches = new Map<string, Map<string, Metadata>>();
const metadataCaches = new Map<string, Map<string, ExportMetadataEntry>>();
let cssInjected = false;
/**
@@ -20,9 +12,9 @@ let cssInjected = false;
* CSS class name: `.{category}-{CSS.escape(name)}`
* e.g. `.item-iron-plate`, `.recipe-iron-plate`, `.signal-signal-red`, `.planet-nauvis`
*/
function useSpriteMetadata(category: string): Map<string, Metadata> {
function useSpriteMetadata(category: string): Map<string, ExportMetadataEntry> {
const exportManifest = useExportManifest();
const [metadata, setMetadata] = useState<Map<string, Metadata>>(
const [metadata, setMetadata] = useState<Map<string, ExportMetadataEntry>>(
metadataCaches.get(category) ?? new Map()
);
@@ -41,7 +33,7 @@ function useSpriteMetadata(category: string): Map<string, Metadata> {
return;
}
const data: [string, Metadata][] = await response.json();
const data: [string, ExportMetadataEntry][] = await response.json();
if (!cssInjected) {
const sheetUrl = `${staticRoot}static/${exportManifest.assets["spritesheet"]}`;
@@ -62,7 +54,7 @@ function useSpriteMetadata(category: string): Map<string, Metadata> {
cssInjected = true;
}
const cache = new Map<string, Metadata>();
const cache = new Map<string, ExportMetadataEntry>();
for (const [key, meta] of data) {
if (meta.category === category) {
const name = key.includes(".") ? key.slice(key.indexOf(".") + 1) : key;
+2 -2
View File
@@ -41,9 +41,9 @@ function StoragePage() {
function getLocaleName(itemName: string) {
let localeName = itemName;
let meta = itemMetadata.get(itemName);
if (meta && meta.localised_name) {
if (meta && (meta as any).localised_name) {
// TODO: implement the locale to name conversion.
localeName = locale.get(meta.localised_name[0])!;
localeName = locale.get((meta as any).localised_name[0])!;
} else {
for (let section of ["item-name", "entity-name", "fluid-name", "equipment-name"]) {
let name = locale.get(`${section}.${itemName}`);