From a1ff2433e4bb2fc88f71babd3541a5ecaeacd184 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Thu, 26 Mar 2026 13:10:58 -0400 Subject: [PATCH] feat: refactor texture resolution logic to streamline item icon rendering --- web/client/src/components/ItemIcon.jsx | 395 ++----------------------- 1 file changed, 32 insertions(+), 363 deletions(-) diff --git a/web/client/src/components/ItemIcon.jsx b/web/client/src/components/ItemIcon.jsx index 65c8213..04e23b0 100644 --- a/web/client/src/components/ItemIcon.jsx +++ b/web/client/src/components/ItemIcon.jsx @@ -2,70 +2,17 @@ import React, { useState } from 'react'; import { getItemEmoji } from '../utils/itemUtils'; import './ItemIcon.css'; -// All textures are proxied & cached through our server -const TEXTURE_PROXY_BASE = '/api/texture'; +// Server-side smart resolution endpoint — handles aliases, animated frames, +// carpet→wool, wood→log, derivatives, CC:Tweaked, Create, and prefix matching. +const RESOLVE_BASE = '/api/texture/resolve'; -// Items whose texture file name differs from their registry name -const TEXTURE_ALIASES = { - // Renamed items in 1.20+ - grass: 'short_grass', - scute: 'turtle_scute', - // Animated items — pick a representative frame - compass: 'compass_16', - recovery_compass: 'recovery_compass_16', - clock: 'clock_22', - // Texture file named differently from item ID - magma_block: 'magma', - crossbow: 'crossbow_standby', - enchanted_golden_apple: 'golden_apple', - light: 'light_15', - // "Smooth" variants use their parent's face texture - smooth_sandstone: 'sandstone_top', - smooth_red_sandstone: 'red_sandstone_top', - smooth_quartz: 'quartz_block_bottom', -}; - -// CC:Tweaked texture paths (registry name → actual file in the CC repo) -const CC_TEXTURE_MAP = { - turtle_normal: 'block/turtle_normal_front', - turtle_advanced: 'block/turtle_advanced_front', - computer_normal: 'block/computer_normal_front', - computer_advanced: 'block/computer_advanced_front', - computer_command: 'block/computer_command_front', - monitor_normal: 'block/monitor_normal_0', - monitor_advanced: 'block/monitor_advanced_0', - wired_modem: 'block/wired_modem_face', - wired_modem_full: 'block/wired_modem_face', - wireless_modem_normal: 'block/wireless_modem_normal_face', - wireless_modem_advanced: 'block/wireless_modem_advanced_face', - speaker: 'block/speaker_front', - disk_drive: 'block/disk_drive_front', - printer: 'block/printer_front_empty', - cable: 'block/cable_core', - // CC item textures - pocket_computer_normal: 'item/pocket_computer_normal', - pocket_computer_advanced: 'item/pocket_computer_advanced', - disk: 'item/disk_frame', - printed_book: 'item/printed_book', - printed_page: 'item/printed_page', - printed_pages: 'item/printed_pages', -}; - -// Create mod — items whose textures live in nested directories -const CREATE_TEXTURE_MAP = { - // Stone types (deeply nested in palettes/) - veridium: 'block/palettes/stone_types/natural/veridium_0', - scorchia: 'block/palettes/stone_types/scorchia', - asurine: 'block/palettes/stone_types/natural/asurine_0', - crimsite: 'block/palettes/stone_types/natural/crimsite_0', - ochrum: 'block/palettes/stone_types/natural/ochrum_0', -}; - -// Wood types for carpet/wood/log resolution -const WOOD_TYPE_SET = new Set([ - 'oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak', - 'mangrove', 'cherry', 'bamboo', 'crimson', 'warped', 'pale_oak', +// Items rendered as 3D entities in-game — no flat texture exists. +// Skip to emoji immediately to avoid a wasted network request. +const ENTITY_ONLY = new Set([ + 'chest', 'ender_chest', 'trapped_chest', 'shield', 'conduit', + 'bell', 'decorated_pot', 'trident', ]); + const DYE_COLORS = new Set([ 'white', 'orange', 'magenta', 'light_blue', 'yellow', 'lime', 'pink', 'gray', 'light_gray', 'cyan', 'purple', 'blue', @@ -73,301 +20,29 @@ const DYE_COLORS = new Set([ ]); /** - * Resolve item names that follow a pattern (carpets, wood, etc.) - * to their actual texture name + folder. Returns { name, isBlock } or null. + * Check if an item is known to have no flat texture (entity/model-only). */ -function resolveDynamicAlias(name) { - // Carpets use their wool texture: white_carpet → white_wool (block/) - if (name.endsWith('_carpet')) { - const color = name.slice(0, -'_carpet'.length); - if (DYE_COLORS.has(color)) { - return { name: `${color}_wool`, isBlock: true }; - } - } - - // Wood (all-bark block) uses log texture: spruce_wood → spruce_log (block/) - if (name.endsWith('_wood')) { - const base = name.slice(0, -'_wood'.length); - const stripped = base.startsWith('stripped_') ? base.slice('stripped_'.length) : null; - const woodType = stripped || base; - if (WOOD_TYPE_SET.has(woodType)) { - return { name: stripped ? `stripped_${woodType}_log` : `${woodType}_log`, isBlock: true }; - } - } - - // Beds, banners, skulls/heads — entity-only textures, skip to emoji immediately - if (name.endsWith('_bed') && DYE_COLORS.has(name.slice(0, -'_bed'.length))) return { name: null }; - if (name.endsWith('_banner') && DYE_COLORS.has(name.slice(0, -'_banner'.length))) return { name: null }; - if (name.endsWith('_skull') || name.endsWith('_head')) return { name: null }; - - // Other entity/3D-model-only items with no flat texture - const ENTITY_ONLY = new Set([ - 'chest', 'ender_chest', 'trapped_chest', 'shield', 'conduit', - 'bell', 'decorated_pot', 'trident', - ]); - if (ENTITY_ONLY.has(name)) return { name: null }; - - return null; -} - -// Items whose texture lives in the block/ folder instead of item/ -const BLOCK_TEXTURES = new Set([ - 'stone', 'granite', 'polished_granite', 'diorite', 'polished_diorite', 'andesite', - 'polished_andesite', 'cobblestone', 'oak_planks', 'spruce_planks', 'birch_planks', - 'jungle_planks', 'acacia_planks', 'dark_oak_planks', 'mangrove_planks', 'cherry_planks', - 'bamboo_planks', 'crimson_planks', 'warped_planks', 'oak_log', 'spruce_log', 'birch_log', - 'jungle_log', 'acacia_log', 'dark_oak_log', 'mangrove_log', 'cherry_log', 'bamboo_block', - 'stripped_oak_log', 'stripped_spruce_log', 'stripped_birch_log', 'stripped_jungle_log', - 'stripped_acacia_log', 'stripped_dark_oak_log', 'stripped_mangrove_log', 'stripped_cherry_log', - 'sand', 'red_sand', 'gravel', 'dirt', 'coarse_dirt', 'rooted_dirt', 'mud', - 'cobblestone', 'mossy_cobblestone', 'obsidian', 'crying_obsidian', - 'netherrack', 'soul_sand', 'soul_soil', 'basalt', 'polished_basalt', 'smooth_basalt', - 'glowstone', 'glass', 'tinted_glass', - - 'bricks', 'stone_bricks', 'mossy_stone_bricks', 'cracked_stone_bricks', - 'chiseled_stone_bricks', 'deepslate_bricks', 'nether_bricks', 'red_nether_bricks', - 'bookshelf', 'clay', 'pumpkin', 'carved_pumpkin', 'jack_o_lantern', 'melon', - 'sponge', 'wet_sponge', 'sandstone', 'red_sandstone', 'prismarine', 'dark_prismarine', - 'sea_lantern', 'hay_block', 'terracotta', 'packed_ice', 'blue_ice', - 'snow_block', 'ice', 'mycelium', 'podzol', 'grass_block', 'moss_block', - 'deepslate', 'cobbled_deepslate', 'polished_deepslate', 'calcite', 'tuff', 'dripstone_block', - 'coal_ore', 'iron_ore', 'gold_ore', 'diamond_ore', 'emerald_ore', 'lapis_ore', 'redstone_ore', - 'copper_ore', 'nether_gold_ore', 'nether_quartz_ore', 'ancient_debris', - 'deepslate_coal_ore', 'deepslate_iron_ore', 'deepslate_gold_ore', 'deepslate_diamond_ore', - 'deepslate_emerald_ore', 'deepslate_lapis_ore', 'deepslate_redstone_ore', 'deepslate_copper_ore', - 'coal_block', 'iron_block', 'gold_block', 'diamond_block', 'emerald_block', - 'lapis_block', 'redstone_block', 'copper_block', 'raw_iron_block', 'raw_gold_block', - 'raw_copper_block', 'netherite_block', 'amethyst_block', 'quartz_block', - 'tnt', 'end_stone', 'end_stone_bricks', 'purpur_block', 'purpur_pillar', - 'magma_block', 'bone_block', 'dried_kelp_block', 'honeycomb_block', - 'slime_block', 'honey_block', 'note_block', 'jukebox', - 'white_wool', 'orange_wool', 'magenta_wool', 'light_blue_wool', 'yellow_wool', - 'lime_wool', 'pink_wool', 'gray_wool', 'light_gray_wool', 'cyan_wool', - 'purple_wool', 'blue_wool', 'brown_wool', 'green_wool', 'red_wool', 'black_wool', - 'white_concrete', 'orange_concrete', 'magenta_concrete', 'light_blue_concrete', - 'yellow_concrete', 'lime_concrete', 'pink_concrete', 'gray_concrete', - 'light_gray_concrete', 'cyan_concrete', 'purple_concrete', 'blue_concrete', - 'brown_concrete', 'green_concrete', 'red_concrete', 'black_concrete', - 'white_terracotta', 'orange_terracotta', 'magenta_terracotta', 'light_blue_terracotta', - 'yellow_terracotta', 'lime_terracotta', 'pink_terracotta', 'gray_terracotta', - 'light_gray_terracotta', 'cyan_terracotta', 'purple_terracotta', 'blue_terracotta', - 'brown_terracotta', 'green_terracotta', 'red_terracotta', 'black_terracotta', - 'white_glazed_terracotta', 'orange_glazed_terracotta', 'magenta_glazed_terracotta', - 'light_blue_glazed_terracotta', 'yellow_glazed_terracotta', 'lime_glazed_terracotta', - 'pink_glazed_terracotta', 'gray_glazed_terracotta', 'light_gray_glazed_terracotta', - 'cyan_glazed_terracotta', 'purple_glazed_terracotta', 'blue_glazed_terracotta', - 'brown_glazed_terracotta', 'green_glazed_terracotta', 'red_glazed_terracotta', - 'black_glazed_terracotta', - 'white_stained_glass', 'orange_stained_glass', 'magenta_stained_glass', - 'light_blue_stained_glass', 'yellow_stained_glass', 'lime_stained_glass', - 'pink_stained_glass', 'gray_stained_glass', 'light_gray_stained_glass', - 'cyan_stained_glass', 'purple_stained_glass', 'blue_stained_glass', - 'brown_stained_glass', 'green_stained_glass', 'red_stained_glass', - 'black_stained_glass', - 'crafting_table', 'furnace', 'blast_furnace', 'smoker', 'smithing_table', - 'fletching_table', 'cartography_table', 'loom', 'stonecutter', 'grindstone', - 'anvil', 'chipped_anvil', 'damaged_anvil', 'enchanting_table', - 'brewing_stand', 'cauldron', 'composter', 'barrel', - 'shulker_box', 'dispenser', 'dropper', 'hopper', 'observer', - 'piston', 'sticky_piston', 'redstone_lamp', 'target', 'lever', - 'beacon', 'conduit', 'lodestone', 'respawn_anchor', - 'cactus', 'sugar_cane', 'bamboo', - 'mushroom_stem', 'brown_mushroom_block', 'red_mushroom_block', - 'oak_leaves', 'spruce_leaves', 'birch_leaves', 'jungle_leaves', - 'acacia_leaves', 'dark_oak_leaves', 'mangrove_leaves', 'cherry_leaves', 'azalea_leaves', - // Trapdoors - 'oak_trapdoor', 'spruce_trapdoor', 'birch_trapdoor', 'jungle_trapdoor', - 'acacia_trapdoor', 'dark_oak_trapdoor', 'mangrove_trapdoor', 'cherry_trapdoor', - 'bamboo_trapdoor', 'crimson_trapdoor', 'warped_trapdoor', 'iron_trapdoor', - 'copper_trapdoor', 'exposed_copper_trapdoor', 'weathered_copper_trapdoor', - 'oxidized_copper_trapdoor', 'pale_oak_trapdoor', - // Dirt path and magma - 'dirt_path', 'magma', - // Aliased texture names that live in block/ (resolved via TEXTURE_ALIASES) - 'sandstone_top', 'red_sandstone_top', 'quartz_block_bottom', - // Additional blocks commonly seen as items - 'smooth_stone', 'smooth_sandstone', 'smooth_red_sandstone', 'smooth_quartz', - 'chiseled_sandstone', 'cut_sandstone', 'chiseled_red_sandstone', 'cut_red_sandstone', - 'quartz_pillar', 'chiseled_quartz_block', 'quartz_bricks', - 'ladder', 'cobweb', 'torch', 'soul_torch', 'lantern', 'soul_lantern', - 'redstone_torch', 'chain', - 'rail', 'powered_rail', 'detector_rail', 'activator_rail', - 'brown_mushroom', 'red_mushroom', - 'oak_sapling', 'spruce_sapling', 'birch_sapling', 'jungle_sapling', - 'acacia_sapling', 'dark_oak_sapling', 'cherry_sapling', - 'dandelion', 'poppy', 'blue_orchid', 'allium', 'azure_bluet', - 'red_tulip', 'orange_tulip', 'white_tulip', 'pink_tulip', - 'oxeye_daisy', 'cornflower', 'lily_of_the_valley', 'sunflower', - 'lilac', 'rose_bush', 'peony', 'wither_rose', 'torchflower', - 'dead_bush', 'fern', 'short_grass', 'tall_grass', 'large_fern', - 'vine', 'lily_pad', 'seagrass', 'kelp', 'hanging_roots', 'spore_blossom', - 'tube_coral', 'brain_coral', 'bubble_coral', 'fire_coral', 'horn_coral', - 'tube_coral_block', 'brain_coral_block', 'bubble_coral_block', 'fire_coral_block', 'horn_coral_block', - 'tube_coral_fan', 'brain_coral_fan', 'bubble_coral_fan', 'fire_coral_fan', 'horn_coral_fan', - 'white_concrete_powder', 'orange_concrete_powder', 'magenta_concrete_powder', - 'light_blue_concrete_powder', 'yellow_concrete_powder', 'lime_concrete_powder', - 'pink_concrete_powder', 'gray_concrete_powder', 'light_gray_concrete_powder', - 'cyan_concrete_powder', 'purple_concrete_powder', 'blue_concrete_powder', - 'brown_concrete_powder', 'green_concrete_powder', 'red_concrete_powder', 'black_concrete_powder', - 'sculk', 'sculk_catalyst', 'sculk_shrieker', 'sculk_sensor', 'sculk_vein', - 'mud_bricks', 'packed_mud', 'muddy_mangrove_roots', - 'crimson_stem', 'warped_stem', 'stripped_crimson_stem', 'stripped_warped_stem', - 'crimson_nylium', 'warped_nylium', 'shroomlight', 'nether_wart_block', 'warped_wart_block', - 'crying_obsidian', 'blackstone', 'polished_blackstone', 'polished_blackstone_bricks', - 'chiseled_polished_blackstone', 'gilded_blackstone', 'cracked_polished_blackstone_bricks', - 'lodestone', 'respawn_anchor', - 'pointed_dripstone', 'moss_carpet', 'azalea', 'flowering_azalea', - 'powder_snow', 'mangrove_roots', - 'copper_block', 'exposed_copper', 'weathered_copper', 'oxidized_copper', - 'cut_copper', 'exposed_cut_copper', 'weathered_cut_copper', 'oxidized_cut_copper', - 'waxed_copper_block', 'waxed_exposed_copper', 'waxed_weathered_copper', 'waxed_oxidized_copper', -]); - -// Some blocks need a specific texture suffix (e.g. furnace_front, oak_log_top) -// We use _front, _top, or _side variants for recognizable look -const BLOCK_TEXTURE_SUFFIXES = { - furnace: '_front', - blast_furnace: '_front', - smoker: '_front', - dispenser: '_front', - dropper: '_front', - observer: '_front', - piston: '_top', - sticky_piston: '_top', - barrel: '_top', - crafting_table: '_top', - cartography_table: '_top', - fletching_table: '_top', - smithing_table: '_top', - grass_block: '_top', - mycelium: '_top', - podzol: '_top', - dirt_path: '_top', - quartz_block: '_side', - pumpkin: '_side', - carved_pumpkin: '_front', - jack_o_lantern: '_front', - jukebox: '_top', - loom: '_front', - bee_nest: '_front', - beehive: '_front', - respawn_anchor: '_top', - bone_block: '_side', - basalt: '_side', - polished_basalt: '_side', - purpur_pillar: '_side', - tnt: '_side', - composter: '_side', - enchanting_table: '_top', -}; - -// Resolve derivative blocks (stairs, slabs, fences, walls, buttons) to parent block texture -const WOOD_TYPES = new Set([ - 'oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak', - 'mangrove', 'cherry', 'bamboo', 'crimson', 'warped', -]); -const STONE_ALIASES = { - brick: 'bricks', stone_brick: 'stone_bricks', mossy_stone_brick: 'mossy_stone_bricks', - nether_brick: 'nether_bricks', red_nether_brick: 'red_nether_bricks', - end_stone_brick: 'end_stone_bricks', deepslate_brick: 'deepslate_bricks', - deepslate_tile: 'deepslate_tiles', polished_blackstone_brick: 'polished_blackstone_bricks', - mud_brick: 'mud_bricks', quartz: 'quartz_block', purpur: 'purpur_block', - smooth_stone: 'smooth_stone', -}; - -function resolveDerivativeTexture(name) { - const suffixes = ['_stairs', '_slab', '_fence_gate', '_fence', '_wall', '_button', '_pressure_plate']; - for (const suffix of suffixes) { - if (!name.endsWith(suffix)) continue; - const base = name.slice(0, -suffix.length); - if (BLOCK_TEXTURES.has(base)) return base; - if (WOOD_TYPES.has(base)) return `${base}_planks`; - if (STONE_ALIASES[base]) return STONE_ALIASES[base]; - return null; - } - return null; +function isEntityOnly(name) { + if (ENTITY_ONLY.has(name)) return true; + if (name.endsWith('_bed') && DYE_COLORS.has(name.slice(0, -'_bed'.length))) return true; + if (name.endsWith('_banner') && DYE_COLORS.has(name.slice(0, -'_banner'.length))) return true; + if (name.endsWith('_skull') || name.endsWith('_head')) return true; + return false; } /** - * Generate texture URLs to try in order. - * - CC:Tweaked → curated texture map (1 request) - * - Create → item/ then block/ (2 requests max) - * - Unknown mods → empty (instant emoji, no wasted requests) - * - Vanilla derivatives → parent block texture (1 request) - * - Vanilla → block/ or item/ with fallback + * Build the single resolve URL for an item. + * The server handles all name resolution (aliases, suffixes, derivatives, etc.) */ -function getTextureUrls(fullItemName) { +function getTextureUrl(fullItemName) { const colonIdx = (fullItemName || '').indexOf(':'); const namespace = colonIdx >= 0 ? fullItemName.substring(0, colonIdx) : 'minecraft'; const shortName = colonIdx >= 0 ? fullItemName.substring(colonIdx + 1) : fullItemName; - const urls = []; - // CC:Tweaked → use curated texture map - if (namespace === 'computercraft') { - const mapped = CC_TEXTURE_MAP[shortName]; - if (mapped) { - urls.push(`${TEXTURE_PROXY_BASE}/computercraft/${mapped}.png`); - } else { - urls.push(`${TEXTURE_PROXY_BASE}/computercraft/item/${shortName}.png`); - urls.push(`${TEXTURE_PROXY_BASE}/computercraft/block/${shortName}.png`); - } - return urls; - } + // Entity-only items → instant emoji, no network request + if (namespace === 'minecraft' && isEntityOnly(shortName)) return null; - // Create mod → curated map first, then item/ then block/ - if (namespace === 'create') { - const mapped = CREATE_TEXTURE_MAP[shortName]; - if (mapped) { - urls.push(`${TEXTURE_PROXY_BASE}/create/${mapped}.png`); - } - urls.push(`${TEXTURE_PROXY_BASE}/create/item/${shortName}.png`); - urls.push(`${TEXTURE_PROXY_BASE}/create/block/${shortName}.png`); - return urls; - } - - // Unknown mod namespace → no textures available, instant emoji fallback - if (namespace !== 'minecraft') { - return urls; - } - - // === Vanilla (minecraft) === - - // Dynamic aliases: carpets→wool, wood→log, entity-only→skip - const dynamic = resolveDynamicAlias(shortName); - if (dynamic) { - if (dynamic.name === null) return urls; // entity-only → instant emoji - if (dynamic.isBlock) { - urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${dynamic.name}.png`); - return urls; - } - } - - const alias = TEXTURE_ALIASES[shortName] || shortName; - - // Derivative blocks (stairs, slabs, fences, walls, buttons) → parent texture - const parent = resolveDerivativeTexture(alias); - if (parent) { - const suffix = BLOCK_TEXTURE_SUFFIXES[parent] || ''; - urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${parent}${suffix}.png`); - return urls; - } - - // Known block → try block/ first (with suffix if applicable) - if (BLOCK_TEXTURES.has(alias)) { - const suffix = BLOCK_TEXTURE_SUFFIXES[alias] || ''; - urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${alias}${suffix}.png`); - if (suffix) urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${alias}.png`); - } - - // Try item/ texture - urls.push(`${TEXTURE_PROXY_BASE}/minecraft/item/${alias}.png`); - - // If not a known block, also try block/ as last resort - if (!BLOCK_TEXTURES.has(alias)) { - urls.push(`${TEXTURE_PROXY_BASE}/minecraft/block/${alias}.png`); - } - - return urls; + return `${RESOLVE_BASE}/${namespace}/${shortName}.png`; } // Cache of resolved icon URLs (avoid re-fetching on every render) @@ -375,21 +50,20 @@ const iconCache = new Map(); /** * Renders a Minecraft item icon using the official game textures. - * Cascading fallback: mod texture → item texture → block texture → emoji + * The server's /api/texture/resolve endpoint does all the smart matching. + * Falls back to emoji when no texture is found. */ function ItemIcon({ itemName, size = 32 }) { - const [urlIndex, setUrlIndex] = useState(0); - const [allFailed, setAllFailed] = useState(false); + const [failed, setFailed] = useState(false); if (!itemName) { return 📦; } - // Use full item name as cache key (handles mod namespaces correctly) const cacheKey = itemName.replace(/^minecraft:/, ''); // Check if we already know this item has no texture - if (iconCache.get(cacheKey) === 'none' || allFailed) { + if (iconCache.get(cacheKey) === 'none' || failed) { return ( {getItemEmoji(itemName)} @@ -412,10 +86,9 @@ function ItemIcon({ itemName, size = 32 }) { ); } - const urls = getTextureUrls(itemName); - const currentUrl = urls[urlIndex]; + const url = getTextureUrl(itemName); - if (!currentUrl) { + if (!url) { iconCache.set(cacheKey, 'none'); return ( @@ -427,21 +100,17 @@ function ItemIcon({ itemName, size = 32 }) { return ( {cacheKey} { - iconCache.set(cacheKey, currentUrl); + iconCache.set(cacheKey, url); }} onError={() => { - if (urlIndex + 1 < urls.length) { - setUrlIndex(urlIndex + 1); - } else { - iconCache.set(cacheKey, 'none'); - setAllFailed(true); - } + iconCache.set(cacheKey, 'none'); + setFailed(true); }} /> );