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.
This commit is contained in:
Hornwitser committed 2026-03-12 18:26:04 +01:00
1 parent f4916594b8
commit 3b8058de81
8 files changed
+482 -185

No files matched your search

+1
View File
@@ -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",
];
+1
View File
@@ -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)
+114 -145
View File
@@ -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<string>): ItemPrototype[] {
function filterAndFixPrototypesWithIcons(prototypes: lib.FactorioPrototypes, allowedTypes: Set<string>) {
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<string, lib.ExportMetadataEntry>;
metadata: Record<string, lib.ExportMetadataEntry[]>;
iconCache: IconCache;
simpleIcons: Map<string, number>;
pos: number;
@@ -342,7 +287,7 @@ const SHEET_WIDTH = 1024;
async function createSheetState(): Promise<SheetState> {
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<string, string>,
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<string, string>,
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<string> }[] = [
{ 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;
}
+294
View File
@@ -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,
},
};
+15 -6
View File
@@ -105,16 +105,25 @@ export type FactorioModSettingPrototype =
* @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 FactorioDefinesValue = number | string | { [index: string]: FactorioDefinesValue };
export type FactorioDefines = {
[index: string]: FactorioDefinesValue,
prototypes: Record<string, Record<string, number>>,
};
export type ExportSettings = FactorioPrototypes<FactorioModSettingPrototype>;
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<string, ExportMetadataEntry[]>;
export type ExportDefines = FactorioDefines;
@@ -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<Category, string> = {
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<Category, Map<string, { size: number; path?: string }>> = {
item, recipe, signal, technology, planet, quality, entity,
const byCategory: Record<Category, Map<string, { icon?: { path?: string }}>> = {
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() {
+20 -17
View File
@@ -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<string, Map<string, ExportMetadataEntry>>();
@@ -33,33 +33,35 @@ function useSpriteMetadata(category: string): Map<string, ExportMetadataEntry> {
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<string, ExportMetadataEntry>();
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<string, ExportMetadataEntry> {
}
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"); }
+16 -4
View File
@@ -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 | ItemFilter>(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 <>
<span className={`factorio-icon item-${hasMeta ? item[1].name : "unknown-item"}`}/>
<span className={`factorio-icon ${type}-${name}`}/>
{localeName}
</>;
},