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} ; },