diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index a47ceb2d..fc700b71 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -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) { diff --git a/packages/host/src/export.ts b/packages/host/src/export.ts index 719e6e96..7c12a04d 100644 --- a/packages/host/src/export.ts +++ b/packages/host/src/export.ts @@ -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> - /** * 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): ItemPrototype[] { +function filterPrototypes(prototypes: lib.FactorioPrototypes, allowedTypes: Set): 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): It /** Shared state for building a unified spritesheet across all categories. */ interface SheetState { sheet: Jimp; - metadata: Map; + metadata: Map; iconCache: IconCache; simpleIcons: Map; 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)`); diff --git a/packages/lib/browser.ts b/packages/lib/browser.ts index 66b3d5c1..aa0273ea 100644 --- a/packages/lib/browser.ts +++ b/packages/lib/browser.ts @@ -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"; diff --git a/packages/lib/index.ts b/packages/lib/index.ts index d8240ffe..3319cc89 100644 --- a/packages/lib/index.ts +++ b/packages/lib/index.ts @@ -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"; diff --git a/packages/lib/src/data/ModPack.ts b/packages/lib/src/data/ModPack.ts index df3adf7d..29daf36d 100644 --- a/packages/lib/src/data/ModPack.ts +++ b/packages/lib/src/data/ModPack.ts @@ -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, 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); } } diff --git a/packages/lib/src/export.ts b/packages/lib/src/export.ts new file mode 100644 index 00000000..71f3a18b --- /dev/null +++ b/packages/lib/src/export.ts @@ -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 = Record> + +export type ExportSettings = FactorioPrototypes; +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][]; diff --git a/packages/lib/tsconfig.browser.json b/packages/lib/tsconfig.browser.json index 97bcd427..8a87bca4 100644 --- a/packages/lib/tsconfig.browser.json +++ b/packages/lib/tsconfig.browser.json @@ -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", diff --git a/packages/web_ui/src/model/item_metadata.ts b/packages/web_ui/src/model/item_metadata.ts index a170566e..4235546d 100644 --- a/packages/web_ui/src/model/item_metadata.ts +++ b/packages/web_ui/src/model/item_metadata.ts @@ -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>(); +const metadataCaches = new Map>(); 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 { +function useSpriteMetadata(category: string): Map { const exportManifest = useExportManifest(); - const [metadata, setMetadata] = useState>( + const [metadata, setMetadata] = useState>( metadataCaches.get(category) ?? new Map() ); @@ -41,7 +33,7 @@ function useSpriteMetadata(category: string): Map { 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 { cssInjected = true; } - const cache = new Map(); + const cache = new Map(); for (const [key, meta] of data) { if (meta.category === category) { const name = key.includes(".") ? key.slice(key.indexOf(".") + 1) : key; diff --git a/plugins/subspace_storage/web/index.tsx b/plugins/subspace_storage/web/index.tsx index 0ad1941f..0ddb3b56 100644 --- a/plugins/subspace_storage/web/index.tsx +++ b/plugins/subspace_storage/web/index.tsx @@ -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}`);