From 3b8058de8113dc52e7238f9b7bab0414a2e55b37 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 12 Mar 2026 18:26:04 +0100 Subject: [PATCH] Refactor icon export to be based on prototypes Use the defines.prototypes data structure from the game to find which prototype sub-types belong in the same base type, and export icons based on these base type. This renames the planets category to space-location, signal to virtual-signal and plits item up into item and fluid. And changes the exported metadata to include prototypes with no icons, as future code might want to use it to for example enumerate all item prototypes. --- packages/controller/src/routes.ts | 1 + packages/host/lua/export/control.lua | 1 + packages/host/src/export.ts | 259 +++++++-------- packages/host/src/export_fallback.ts | 294 ++++++++++++++++++ packages/lib/src/export.ts | 21 +- .../src/components/IconReferencePage.tsx | 34 +- packages/web_ui/src/model/item_metadata.ts | 37 ++- plugins/subspace_storage/web/index.tsx | 20 +- 8 files changed, 482 insertions(+), 185 deletions(-) create mode 100644 packages/host/src/export_fallback.ts diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index fc700b71..8c461c9d 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -241,6 +241,7 @@ async function uploadExport(req: Request, res: Response) { "export/settings.json", "export/prototypes.json", "export/locale.json", + "export/defines.json", "export/spritesheet.png", "export/metadata.json", ]; diff --git a/packages/host/lua/export/control.lua b/packages/host/lua/export/control.lua index 1833a4cc..ae895a8c 100644 --- a/packages/host/lua/export/control.lua +++ b/packages/host/lua/export/control.lua @@ -9,4 +9,5 @@ script.on_init(function() for name, mod_setting in pairs(mod_settings) do send_json("mod_setting_mod", { name = name, mod = mod_setting.mod }) end + send_json("defines", defines) end) diff --git a/packages/host/src/export.ts b/packages/host/src/export.ts index 4a76a4ab..0a401b42 100644 --- a/packages/host/src/export.ts +++ b/packages/host/src/export.ts @@ -7,6 +7,7 @@ import JSZip from "jszip"; import * as lib from "@clusterio/lib"; import * as libBuildMod from "@clusterio/lib/dist/node/build_mod"; import type { FactorioServer } from "./server"; +import { definesPrototypesFallback } from "./export_fallback"; interface SimpleIconSpecification { icons?: never; @@ -28,7 +29,7 @@ interface LayeredIconSpecification { icon_mipmaps?: number; } type IconSpecification = LayeredIconSpecification | SimpleIconSpecification; -type ItemPrototype = lib.FactorioPrototypeBase & IconSpecification; +type PrototypeWithIcon = lib.FactorioPrototypeBase & IconSpecification; /** @@ -241,7 +242,7 @@ async function loadLayeredIcon( return icon; } -function fixIcons(item: ItemPrototype) { +function fixIcons(item: PrototypeWithIcon) { const icons = item.icons; if (typeof icons === "object" && !(icons instanceof Array) && icons !== null) { // It's possible to specify icons as an object with arbitrary keys @@ -253,84 +254,28 @@ function fixIcons(item: ItemPrototype) { return item; } -/** Get the primary icon path from a prototype (first layer for layered, .icon for simple). */ -function getPrimaryIconPath(item: ItemPrototype): string | undefined { - if (item.icons) { - return item.icons[0]?.icon; - } - return item.icon; -} - -const itemTypes = new Set([ +const prototypesWithIcon = [ "item", - "ammo", - "capsule", - "gun", - "item-with-entity-data", - "item-with-label", - "item-with-inventory", - "blueprint-book", - "item-with-tags", - "selection-tool", - "blueprint", - "copy-paste-tool", - "deconstruction-item", - "upgrade-item", - "module", - "rail-planner", - "spidertron-remote", - "tool", - "armor", - "mining-tool", - "repair-tool", - // XXX Bad hack to get icons for fluids. These should be treated as a - // separate namespace from items as they may be named the same as items. "fluid", -]); + "recipe", + "virtual-signal", + "technology", + "space-location", + "quality", + "entity", +] as const; -const recipeTypes = new Set(["recipe"]); -const signalTypes = new Set(["virtual-signal"]); -const technologyTypes = new Set(["technology"]); -const planetTypes = new Set(["planet", "space-location"]); -const qualityTypes = new Set(["quality"]); - -// Entity types to export. Entities whose name matches an already-exported -const entityTypes = new Set([ - "assembling-machine", "furnace", "inserter", "transport-belt", "underground-belt", - "splitter", "loader", "loader-1x1", "mining-drill", "container", "logistic-container", - "electric-pole", "pipe", "pipe-to-ground", "pump", "storage-tank", "boiler", - "generator", "solar-panel", "accumulator", "reactor", "heat-pipe", "lab", "beacon", - "roboport", "lamp", "radar", "wall", "gate", "land-mine", "turret", "ammo-turret", - "electric-turret", "fluid-turret", "artillery-turret", "rocket-silo", "car", - "spider-vehicle", "locomotive", "cargo-wagon", "fluid-wagon", "artillery-wagon", - "train-stop", "rail-signal", "rail-chain-signal", "straight-rail", "curved-rail-a", - "curved-rail-b", "half-diagonal-rail", "elevated-straight-rail", "elevated-curved-rail-a", - "elevated-curved-rail-b", "elevated-half-diagonal-rail", "rail-ramp", "rail-support", - "space-platform-hub", "cargo-bay", "asteroid-collector", "thruster", "cargo-pod", - "cargo-landing-pad", "asteroid", - "construction-robot", "logistic-robot", "combat-robot", "capture-robot", - "unit", "unit-spawner", "spider-unit", "segmented-unit", - "tree", "plant", "fish", "resource", "cliff", "simple-entity", - "simple-entity-with-owner", "simple-entity-with-force", - "arithmetic-combinator", "decider-combinator", "constant-combinator", - "selector-combinator", "programmable-speaker", "power-switch", "display-panel", - "character", - "offshore-pump", "valve", "lightning-attractor", - "fusion-generator", "fusion-reactor", "burner-generator", - "agricultural-tower", "market", "lane-splitter", -]); - -function filterPrototypes(prototypes: lib.FactorioPrototypes, allowedTypes: Set): ItemPrototype[] { +function filterAndFixPrototypesWithIcons(prototypes: lib.FactorioPrototypes, allowedTypes: Set) { return Object.entries(prototypes) .filter(([type]) => allowedTypes.has(type)) - .flatMap(([_, typePrototypes]) => Object.values(typePrototypes) as ItemPrototype[]) + .flatMap(([_, typePrototypes]) => Object.values(typePrototypes) as PrototypeWithIcon[]) .map(fixIcons); } /** Shared state for building a unified spritesheet across all categories. */ interface SheetState { sheet: Image; - metadata: Map; + metadata: Record; iconCache: IconCache; simpleIcons: Map; pos: number; @@ -342,7 +287,7 @@ const SHEET_WIDTH = 1024; async function createSheetState(): Promise { return { sheet: new Jimp({ width: SHEET_WIDTH, height: SHEET_ICON_SIZE }), - metadata: new Map(), + metadata: Object.create(null), iconCache: new Map(), simpleIcons: new Map(), pos: 0, @@ -360,74 +305,92 @@ async function growSheet(state: SheetState) { } } +async function exportIcon( + server: FactorioServer, + modVersions: Map, + state: SheetState, + prototype: PrototypeWithIcon, +) { + let icon: Image | null = null; + let iconPos: number | undefined; + let iconData: lib.ExportMetadataEntry["icon"]; + let iconPath: string | undefined; + + if (prototype.icons) { + icon = await loadLayeredIcon( + server, modVersions, prototype, SHEET_ICON_SIZE, state.iconCache, + ); + iconPos = state.pos; + iconPath = prototype.icons[0].icon; + + } else if (prototype.icon) { + iconPos = state.simpleIcons.get(prototype.icon); + iconPath = prototype.icon; + if (iconPos === undefined) { + icon = await loadSimpleIcon( + server, modVersions, prototype, SHEET_ICON_SIZE, state.iconCache, + ); + if (icon) { + iconPos = state.pos; + state.simpleIcons.set(prototype.icon, state.pos); + } + } + } + + if (iconPos !== undefined) { + iconData = { + x: iconPos * SHEET_ICON_SIZE % SHEET_WIDTH, + y: Math.floor(iconPos / (SHEET_WIDTH / SHEET_ICON_SIZE)) * SHEET_ICON_SIZE, + size: SHEET_ICON_SIZE, + path: iconPath, + }; + } + + if (icon) { + await growSheet(state); + state.sheet.composite( + icon, + state.pos * SHEET_ICON_SIZE % SHEET_WIDTH, + Math.floor(state.pos / (SHEET_WIDTH / SHEET_ICON_SIZE)) * SHEET_ICON_SIZE, + ); + state.pos += 1; + } + + return iconData; +} + /** - * Export item icons for a single category onto the shared spritesheet. + * Export prototype icons for a single base prototype onto the shared spritesheet. * * @param server - The server to load icons from. * @param modVersions - Mapping of mod name to version. - * @param items - Array of item prototypes to export. - * @param category - Category name for metadata. + * @param prototypes - Array of prototypes with icon to export for this base type. + * @param baseName - name of base type (used in metadata). * @param state - Shared spritesheet state (mutated in place). - * @returns Number of icons exported for this category. + * @returns Number of icons exported for this base type. * @internal */ -async function exportItems( +async function exportIcons( server: FactorioServer, modVersions: Map, - items: ItemPrototype[], - category: string, + prototypes: PrototypeWithIcon[], + baseName: string, state: SheetState, ) { + const metadata = state.metadata[baseName] ?? []; + state.metadata[baseName] ??= metadata; let count = 0; - for (let item of items) { - // Skip prototypes with no icon data at all - if (!item.icons && !item.icon) { - continue; - } - - let icon: Image | null = null; - let iconPos: number | undefined; - if (item.icons) { - icon = await loadLayeredIcon( - server, modVersions, item, SHEET_ICON_SIZE, state.iconCache, - ); - iconPos = state.pos; - - } else { - iconPos = state.simpleIcons.get(item.icon as string); - if (iconPos === undefined) { - icon = await loadSimpleIcon( - server, modVersions, item, SHEET_ICON_SIZE, state.iconCache, - ); - if (icon) { - iconPos = state.pos; - state.simpleIcons.set(item.icon as string, state.pos); - } - } - } - - if (iconPos !== undefined) { - const iconPath = getPrimaryIconPath(item); - state.metadata.set(`${category}.${item.name}`, { - x: iconPos * SHEET_ICON_SIZE % SHEET_WIDTH, - y: Math.floor(iconPos / (SHEET_WIDTH / SHEET_ICON_SIZE)) * SHEET_ICON_SIZE, - size: SHEET_ICON_SIZE, - localised_name: item.localised_name, - category, - ...(iconPath ? { path: iconPath } : {}), - }); - } - + for (let prototype of prototypes) { + const icon = await exportIcon(server, modVersions, state, prototype); if (icon) { - await growSheet(state); - state.sheet.composite( - icon, - state.pos * SHEET_ICON_SIZE % SHEET_WIDTH, - Math.floor(state.pos / (SHEET_WIDTH / SHEET_ICON_SIZE)) * SHEET_ICON_SIZE, - ); - state.pos += 1; count += 1; } + metadata.push({ + name: prototype.name, + type: prototype.type, + localised_name: prototype.localised_name, + icon, + }); } return count; @@ -501,7 +464,7 @@ async function exportLocale( } /** - * Export the locale and item icons for the given factorio server + * Export locale, icons and metadata for the given factorio server * * @param server - The server to export the data from. * @returns zip file with exported data. @@ -513,6 +476,7 @@ export async function exportData(server: FactorioServer) { let prototypes: lib.ExportPrototypes = {}; let modVersions = new Map(); let modOrder: string[] = []; + let defines: lib.FactorioDefines | undefined; function add(obj: lib.FactorioPrototypes, prototype: lib.FactorioPrototypeBase) { if (!Object.prototype.hasOwnProperty.call(obj, prototype.type)) { @@ -533,6 +497,7 @@ export async function exportData(server: FactorioServer) { server._logger.error(`Unable to find ${name} in settings prototypes`); }); server.on("ipc-mod_list", data => { modVersions = new Map(Object.entries(data)); }); + server.on("ipc-defines", data => { defines = data; }); server.on("output", parsed => { if (parsed.format === "seconds" && parsed.type === "generic") { let match = /^Checksum of (.*): \d+$/.exec(parsed.message); @@ -553,6 +518,15 @@ export async function exportData(server: FactorioServer) { throw new Error("No prototypes got exported"); } + if (!defines) { + throw new Error("No defines got exported"); + } + + // compat with versions prior to 1.1.31 + if (!defines.prototypes) { + defines.prototypes = definesPrototypesFallback; + } + // Some mod authors put leading zeros into the versions of their zip files. let splitter = /^(.*)_(\d+)\.(\d+)\.(\d+)\.zip?$/; for (let entry of await fs.readdir(server.writePath("mods"))) { @@ -575,45 +549,40 @@ export async function exportData(server: FactorioServer) { } } - const categories: { name: string, types: Set }[] = [ - { name: "item", types: itemTypes }, - { name: "recipe", types: recipeTypes }, - { name: "signal", types: signalTypes }, - { name: "technology", types: technologyTypes }, - { name: "planet", types: planetTypes }, - { name: "quality", types: qualityTypes }, - { name: "entity", types: entityTypes }, - ]; - let locale = await exportLocale(server, modVersions, modOrder, "en"); // Free up the memory used by zip files loaded during the export. zipCache.clear(); let zip = new JSZip(); - zip.file("export/settings.json", JSON.stringify(settings)); - zip.file("export/prototypes.json", JSON.stringify(prototypes)); + zip.file("export/settings.json", JSON.stringify(settings satisfies lib.ExportSettings)); + zip.file("export/prototypes.json", JSON.stringify(prototypes satisfies lib.ExportPrototypes)); zip.file("export/locale.json", JSON.stringify([...locale.entries()] satisfies lib.ExportLocale)); + zip.file("export/defines.json", JSON.stringify(defines satisfies lib.ExportDefines)); - // Build a single unified spritesheet across all prototype categories. + // Build a single unified spritesheet across all prototypes with icons. const state = await createSheetState(); - for (const category of categories) { - const categoryItems = filterPrototypes(prototypes, category.types); - - if (categoryItems.length === 0) { + for (const baseName of prototypesWithIcon) { + if (!Object.prototype.hasOwnProperty.call(defines.prototypes, baseName)) { continue; } - const count = await exportItems(server, modVersions, categoryItems, category.name, state); - server._logger.info(`Exported ${count} ${category.name} icons`); + const subTypeNames = new Set(Object.keys(defines.prototypes[baseName])); + const prototypesWithIcons = filterAndFixPrototypesWithIcons(prototypes, subTypeNames); + + if (prototypesWithIcons.length === 0) { + continue; + } + const count = await exportIcons(server, modVersions, prototypesWithIcons, baseName, state); + server._logger.info(`Exported ${count} ${baseName} icons`); } // Crop sheet to actual used height and write single spritesheet + metadata. const usedRows = Math.ceil(state.pos / (SHEET_WIDTH / SHEET_ICON_SIZE)); state.sheet.crop({ x: 0, y: 0, w: SHEET_WIDTH, h: Math.max(usedRows * SHEET_ICON_SIZE, 1) }); zip.file("export/spritesheet.png", await state.sheet.getBuffer(JimpMime.png)); - zip.file("export/metadata.json", JSON.stringify([...state.metadata.entries()] satisfies lib.ExportMetadata)); + zip.file("export/metadata.json", JSON.stringify(state.metadata satisfies lib.ExportMetadata)); - server._logger.info(`Export complete: ${state.metadata.size} icons on ${usedRows} row(s)`); + server._logger.info(`Export complete: ${state.pos} icons on ${usedRows} row(s)`); return zip; } diff --git a/packages/host/src/export_fallback.ts b/packages/host/src/export_fallback.ts new file mode 100644 index 00000000..75b03163 --- /dev/null +++ b/packages/host/src/export_fallback.ts @@ -0,0 +1,294 @@ +// defines.prototypes from Factorio 1.1.31, the first version that has this. +export const definesPrototypesFallback = { + "achievement": { + "achievement": 0, + "build-entity-achievement": 0, + "combat-robot-count": 0, + "construct-with-robots-achievement": 0, + "deconstruct-with-robots-achievement": 0, + "deliver-by-robots-achievement": 0, + "dont-build-entity-achievement": 0, + "dont-craft-manually-achievement": 0, + "dont-use-entity-in-energy-production-achievement": 0, + "finish-the-game-achievement": 0, + "group-attack-achievement": 0, + "kill-achievement": 0, + "player-damaged-achievement": 0, + "produce-achievement": 0, + "produce-per-hour-achievement": 0, + "research-achievement": 0, + "train-path-achievement": 0, + }, + "ambient-sound": { + "ambient-sound": 0, + }, + "ammo-category": { + "ammo-category": 0, + }, + "animation": { + "animation": 0, + }, + "autoplace-control": { + "autoplace-control": 0, + }, + "custom-input": { + "custom-input": 0, + }, + "damage-type": { + "damage-type": 0, + }, + "decorative": { + "optimized-decorative": 0, + }, + "editor-controller": { + "editor-controller": 0, + }, + "entity": { + "accumulator": 0, + "ammo-turret": 0, + "arithmetic-combinator": 0, + "arrow": 0, + "artillery-flare": 0, + "artillery-projectile": 0, + "artillery-turret": 0, + "artillery-wagon": 0, + "assembling-machine": 0, + "beacon": 0, + "beam": 0, + "boiler": 0, + "burner-generator": 0, + "car": 0, + "cargo-wagon": 0, + "character": 0, + "character-corpse": 0, + "cliff": 0, + "combat-robot": 0, + "constant-combinator": 0, + "construction-robot": 0, + "container": 0, + "corpse": 0, + "curved-rail": 0, + "decider-combinator": 0, + "deconstructible-tile-proxy": 0, + "electric-energy-interface": 0, + "electric-pole": 0, + "electric-turret": 0, + "entity-ghost": 0, + "explosion": 0, + "fire": 0, + "fish": 0, + "flame-thrower-explosion": 0, + "fluid-turret": 0, + "fluid-wagon": 0, + "flying-text": 0, + "furnace": 0, + "gate": 0, + "generator": 0, + "heat-interface": 0, + "heat-pipe": 0, + "highlight-box": 0, + "infinity-container": 0, + "infinity-pipe": 0, + "inserter": 0, + "item-entity": 0, + "item-request-proxy": 0, + "lab": 0, + "lamp": 0, + "land-mine": 0, + "leaf-particle": 0, + "linked-belt": 0, + "linked-container": 0, + "loader": 0, + "loader-1x1": 0, + "locomotive": 0, + "logistic-container": 0, + "logistic-robot": 0, + "market": 0, + "mining-drill": 0, + "offshore-pump": 0, + "particle": 0, + "particle-source": 0, + "pipe": 0, + "pipe-to-ground": 0, + "player-port": 0, + "power-switch": 0, + "programmable-speaker": 0, + "projectile": 0, + "pump": 0, + "radar": 0, + "rail-chain-signal": 0, + "rail-remnants": 0, + "rail-signal": 0, + "reactor": 0, + "resource": 0, + "roboport": 0, + "rocket-silo": 0, + "rocket-silo-rocket": 0, + "rocket-silo-rocket-shadow": 0, + "simple-entity": 0, + "simple-entity-with-force": 0, + "simple-entity-with-owner": 0, + "smoke": 0, + "smoke-with-trigger": 0, + "solar-panel": 0, + "speech-bubble": 0, + "spider-leg": 0, + "spider-vehicle": 0, + "splitter": 0, + "sticker": 0, + "storage-tank": 0, + "straight-rail": 0, + "stream": 0, + "tile-ghost": 0, + "train-stop": 0, + "transport-belt": 0, + "tree": 0, + "turret": 0, + "underground-belt": 0, + "unit": 0, + "unit-spawner": 0, + "wall": 0, + }, + "equipment": { + "active-defense-equipment": 0, + "battery-equipment": 0, + "belt-immunity-equipment": 0, + "energy-shield-equipment": 0, + "generator-equipment": 0, + "movement-bonus-equipment": 0, + "night-vision-equipment": 0, + "roboport-equipment": 0, + "solar-panel-equipment": 0, + }, + "equipment-category": { + "equipment-category": 0, + }, + "equipment-grid": { + "equipment-grid": 0, + }, + "fluid": { + "fluid": 0, + }, + "font": { + "font": 0, + }, + "fuel-category": { + "fuel-category": 0, + }, + "god-controller": { + "god-controller": 0, + }, + "gui-style": { + "gui-style": 0, + }, + "item": { + "ammo": 0, + "armor": 0, + "blueprint": 0, + "blueprint-book": 0, + "capsule": 0, + "copy-paste-tool": 0, + "deconstruction-item": 0, + "gun": 0, + "item": 0, + "item-with-entity-data": 0, + "item-with-inventory": 0, + "item-with-label": 0, + "item-with-tags": 0, + "mining-tool": 0, + "module": 0, + "rail-planner": 0, + "repair-tool": 0, + "selection-tool": 0, + "spidertron-remote": 0, + "tool": 0, + "upgrade-item": 0, + }, + "item-group": { + "item-group": 0, + }, + "item-subgroup": { + "item-subgroup": 0, + }, + "map-gen-presets": { + "map-gen-presets": 0, + }, + "map-settings": { + "map-settings": 0, + }, + "module-category": { + "module-category": 0, + }, + "mouse-cursor": { + "mouse-cursor": 0, + }, + "noise-expression": { + "noise-expression": 0, + }, + "noise-layer": { + "noise-layer": 0, + }, + "particle": { + "optimized-particle": 0, + }, + "recipe": { + "recipe": 0, + }, + "recipe-category": { + "recipe-category": 0, + }, + "resource-category": { + "resource-category": 0, + }, + "shortcut": { + "shortcut": 0, + }, + "sound": { + "sound": 0, + }, + "spectator-controller": { + "spectator-controller": 0, + }, + "sprite": { + "sprite": 0, + }, + "technology": { + "technology": 0, + }, + "tile": { + "tile": 0, + }, + "tile-effect": { + "tile-effect": 0, + }, + "tips-and-tricks-item": { + "tips-and-tricks-item": 0, + }, + "tips-and-tricks-item-category": { + "tips-and-tricks-item-category": 0, + }, + "trigger-target-type": { + "trigger-target-type": 0, + }, + "trivial-smoke": { + "trivial-smoke": 0, + }, + "tutorial": { + "tutorial": 0, + }, + "utility-constants": { + "utility-constants": 0, + }, + "utility-sounds": { + "utility-sounds": 0, + }, + "utility-sprites": { + "utility-sprites": 0, + }, + "virtual-signal": { + "virtual-signal": 0, + }, + "wind-sound": { + "wind-sound": 0, + }, +}; diff --git a/packages/lib/src/export.ts b/packages/lib/src/export.ts index d461471a..10f06089 100644 --- a/packages/lib/src/export.ts +++ b/packages/lib/src/export.ts @@ -105,16 +105,25 @@ export type FactorioModSettingPrototype = * @see [Data.raw](https://lua-api.factorio.com/stable/types/Data.html#raw) */ export type FactorioPrototypes = Record> +export type FactorioDefinesValue = number | string | { [index: string]: FactorioDefinesValue }; +export type FactorioDefines = { + [index: string]: FactorioDefinesValue, + prototypes: Record>, +}; export type ExportSettings = FactorioPrototypes; export type ExportPrototypes = FactorioPrototypes; export type ExportLocale = [string, string][]; export interface ExportMetadataEntry { - x: number, - y: number, - size: number, + name: string, + type: string, localised_name?: FactorioLocalisedString, - category: string, - path?: string, + icon?: { + x: number, + y: number, + size: number, + path?: string, + }, } -export type ExportMetadata = [string, ExportMetadataEntry][]; +export type ExportMetadata = Record; +export type ExportDefines = FactorioDefines; diff --git a/packages/web_ui/src/components/IconReferencePage.tsx b/packages/web_ui/src/components/IconReferencePage.tsx index bfb896bf..800bfb0c 100644 --- a/packages/web_ui/src/components/IconReferencePage.tsx +++ b/packages/web_ui/src/components/IconReferencePage.tsx @@ -4,11 +4,12 @@ import { Card, Input, Space, Table, Tag, Typography } from "antd"; import { useItemMetadata, useRecipeMetadata, - useSignalMetadata, + useVirtualSignalMetadata, useTechnologyMetadata, - usePlanetMetadata, + useSpaceLocationMetadata, useQualityMetadata, useEntityMetadata, + useFluidMetadata, } from "../model/item_metadata"; import { useExportManifest } from "../model/export_manifest"; import PageHeader from "./PageHeader"; @@ -21,37 +22,45 @@ type IconEntry = { category: string; name: string; cssClass: string; - size: number; path?: string; }; const CATEGORIES = [ - "item", "recipe", "signal", "technology", "planet", "quality", "entity", + "item", "fluid", "recipe", "virtual-signal", "technology", "space-location", "quality", "entity", ] as const; type Category = typeof CATEGORIES[number]; const CATEGORY_LABELS: Record = { item: "Item", + fluid: "Fluid", recipe: "Recipe", - signal: "Signal", + "virtual-signal": "Virtual Signal", technology: "Technology", - planet: "Planet", + "space-location": "Space Location", quality: "Quality", entity: "Entity", }; function useAllIconEntries() { const item = useItemMetadata(); + const fluid = useFluidMetadata(); const recipe = useRecipeMetadata(); - const signal = useSignalMetadata(); + const virtualSignal = useVirtualSignalMetadata(); const technology = useTechnologyMetadata(); - const planet = usePlanetMetadata(); + const spaceLocation = useSpaceLocationMetadata(); const quality = useQualityMetadata(); const entity = useEntityMetadata(); - const byCategory: Record> = { - item, recipe, signal, technology, planet, quality, entity, + const byCategory: Record> = { + item, + fluid, + recipe, + "virtual-signal": virtualSignal, + technology, + "space-location": spaceLocation, + quality, + entity, }; return useMemo(() => { @@ -66,14 +75,13 @@ function useAllIconEntries() { category, name, cssClass: `${category}-${CSS.escape(name)}`, - size: meta.size, - path: meta.path, + path: meta.icon?.path, }); } } return entries; - }, [item, recipe, signal, technology, planet, quality, entity]); + }, [item, fluid, recipe, virtualSignal, technology, spaceLocation, quality, entity]); } export default function IconReferencePage() { diff --git a/packages/web_ui/src/model/item_metadata.ts b/packages/web_ui/src/model/item_metadata.ts index 4235546d..e8e1bc05 100644 --- a/packages/web_ui/src/model/item_metadata.ts +++ b/packages/web_ui/src/model/item_metadata.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useExportManifest } from "./export_manifest"; -import { ExportMetadataEntry } from "@clusterio/lib"; +import { ExportMetadata, ExportMetadataEntry } from "@clusterio/lib"; const metadataCaches = new Map>(); @@ -33,33 +33,35 @@ function useSpriteMetadata(category: string): Map { return; } - const data: [string, ExportMetadataEntry][] = await response.json(); + const data: ExportMetadata = await response.json(); if (!cssInjected) { const sheetUrl = `${staticRoot}static/${exportManifest.assets["spritesheet"]}`; const style = document.createElement("style"); document.head.appendChild(style); - for (const [key, meta] of data) { - const name = key.includes(".") ? key.slice(key.indexOf(".") + 1) : key; - style.sheet!.insertRule(`\ -.${meta.category}-${CSS.escape(name)} { + for (const [baseName, prototypes] of Object.entries(data)) { + for (const prototype of prototypes) { + const icon = prototype.icon; + if (!icon) { + continue; + } + style.sheet!.insertRule(`\ +.${baseName}-${CSS.escape(prototype.name)} { background-image: url("${sheetUrl}"); background-repeat: no-repeat; - background-position: -${meta.x}px -${meta.y}px; - height: ${meta.size}px; - width: ${meta.size}px; + background-position: -${icon.x}px -${icon.y}px; + height: ${icon.size}px; + width: ${icon.size}px; }` - ); + ); + } } cssInjected = true; } const cache = new Map(); - for (const [key, meta] of data) { - if (meta.category === category) { - const name = key.includes(".") ? key.slice(key.indexOf(".") + 1) : key; - cache.set(name, meta); - } + for (const prototype of data[category] ?? []) { + cache.set(prototype.name, prototype); } metadataCaches.set(category, cache); setMetadata(cache); @@ -74,9 +76,10 @@ function useSpriteMetadata(category: string): Map { } export function useItemMetadata() { return useSpriteMetadata("item"); } +export function useFluidMetadata() { return useSpriteMetadata("fluid"); } export function useRecipeMetadata() { return useSpriteMetadata("recipe"); } -export function useSignalMetadata() { return useSpriteMetadata("signal"); } +export function useVirtualSignalMetadata() { return useSpriteMetadata("virtual-signal"); } export function useTechnologyMetadata() { return useSpriteMetadata("technology"); } -export function usePlanetMetadata() { return useSpriteMetadata("planet"); } +export function useSpaceLocationMetadata() { return useSpriteMetadata("space-location"); } export function useQualityMetadata() { return useSpriteMetadata("quality"); } export function useEntityMetadata() { return useSpriteMetadata("entity"); } diff --git a/plugins/subspace_storage/web/index.tsx b/plugins/subspace_storage/web/index.tsx index 9c81fb02..120d29da 100644 --- a/plugins/subspace_storage/web/index.tsx +++ b/plugins/subspace_storage/web/index.tsx @@ -4,7 +4,7 @@ import { Input, Table, Typography } from "antd"; import * as lib from "@clusterio/lib"; import { BaseWebPlugin, PageLayout, PageHeader, Control, ControlContext, - notifyErrorHandler, useItemMetadata, useLocale, + notifyErrorHandler, useItemMetadata, useFluidMetadata, useLocale, } from "@clusterio/web_ui"; import { GetStorageRequest, Item, SetStorageSubscriptionRequest, UpdateStorageEvent } from "../messages"; @@ -34,13 +34,14 @@ function StoragePage() { let control = useContext(ControlContext); let locale = useLocale(); let itemMetadata = useItemMetadata(); + let fluidMetadata = useFluidMetadata(); let storage = useStorage(control); type ItemFilter = ([name, item]: [string, Item]) => boolean; let [filter, setFilter] = useState(null); function getLocaleName(itemName: string) { let localeName = itemName; - let meta = itemMetadata.get(itemName); + let meta = itemMetadata.get(itemName) ?? fluidMetadata.get(itemName); if (meta && meta.localised_name) { // TODO: implement the locale to name conversion. if (typeof meta.localised_name === "string") { @@ -98,10 +99,21 @@ function StoragePage() { }, render: (_, item) => { let localeName = getLocaleName(item[1].name); - let hasMeta = itemMetadata.get(item[1].name); + let type; + let name; + if (itemMetadata.has(item[1].name)) { + type = "item"; + name = item[1].name; + } else if (fluidMetadata.has(item[1].name)) { + type = "fluid"; + name = item[1].name; + } else { + type = "item"; + name = "unknown-item"; + } return <> - + {localeName} ; },