feat: implement smart texture resolution and indexing for improved asset management

This commit is contained in:
MayaTheShy
2026-03-26 13:11:06 -04:00
parent 981e561c2d
commit 13b374d7f8

View File

@@ -188,11 +188,247 @@ const TEXTURE_UPSTREAMS = {
// Ensure cache directory exists
fs.mkdirSync(TEXTURE_CACHE_DIR, { recursive: true });
// ── Texture index: maps base_name → relative path for smart resolution ────
// Built on startup by scanning the texture cache. Key = short name (no ext),
// Value = array of relative paths (e.g. ["item/diamond.png", "block/diamond_ore.png"])
const textureIndex = {}; // { namespace: { baseName: [relPath, ...] } }
function buildTextureIndex() {
for (const ns of Object.keys(TEXTURE_UPSTREAMS)) {
textureIndex[ns] = {};
const nsDir = path.join(TEXTURE_CACHE_DIR, ns);
if (!fs.existsSync(nsDir)) continue;
(function walk(dir, rel) {
try {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
if (entry.isDirectory()) walk(path.join(dir, entry.name), childRel);
else if (entry.name.endsWith('.png') && !entry.name.endsWith('.miss')) {
const baseName = entry.name.slice(0, -4); // strip .png
if (!textureIndex[ns][baseName]) textureIndex[ns][baseName] = [];
textureIndex[ns][baseName].push(childRel);
}
}
} catch (_) { /* ignore unreadable dirs */ }
})(nsDir, '');
}
const total = Object.values(textureIndex).reduce(
(sum, idx) => sum + Object.keys(idx).length, 0);
console.log(`[Texture Index] Indexed ${total} unique texture names`);
}
buildTextureIndex();
// ── Smart texture resolution ──────────────────────────────────────────────
// Given a Minecraft item/block registry name, find the best matching texture.
// Returns the relative cache path or null.
// Patterns for items whose textures use numbered animation frames
const ANIMATED_ITEMS = new Set([
'compass', 'recovery_compass', 'clock',
'crossbow', // crossbow_standby, crossbow_pulling_0, etc.
'light', // light_0 .. light_15
]);
// Maps a registry short name to the actual texture base name
const SERVER_ALIASES = {
// Registry name differs from texture filename
magma_block: 'magma',
grass: 'short_grass',
scute: 'turtle_scute',
enchanted_golden_apple: 'golden_apple',
};
// Preferred animation frame for animated items
const ANIMATED_FRAME = {
compass: 'compass_16',
recovery_compass: 'recovery_compass_16',
clock: 'clock_22',
crossbow: 'crossbow_standby',
light: 'light_15',
};
// Dye color set for carpet/bed/banner resolution
const DYE_COLORS = new Set([
'white', 'orange', 'magenta', 'light_blue', 'yellow', 'lime',
'pink', 'gray', 'light_gray', 'cyan', 'purple', 'blue',
'brown', 'green', 'red', 'black',
]);
const WOOD_TYPES = new Set([
'oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak',
'mangrove', 'cherry', 'bamboo', 'crimson', 'warped', 'pale_oak',
]);
// Blocks whose texture uses a suffix: name_front, name_top, etc.
const BLOCK_SUFFIXES = {
furnace: '_front_on', blast_furnace: '_front_on', smoker: '_front_on',
dispenser: '_front', dropper: '_front', observer: '_front',
barrel: '_top', crafting_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',
tnt: '_side', composter: '_side', enchanting_table: '_top',
cartography_table: '_top', fletching_table: '_top', smithing_table: '_top',
};
// Derivative block suffixes → resolve to parent block
const DERIVATIVE_SUFFIXES = [
'_stairs', '_slab', '_fence_gate', '_fence', '_wall',
'_button', '_pressure_plate',
];
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',
};
// "smooth_" blocks use the parent's top/bottom face
const SMOOTH_ALIASES = {
smooth_sandstone: 'sandstone_top',
smooth_red_sandstone: 'red_sandstone_top',
smooth_quartz: 'quartz_block_bottom',
};
function resolveTexturePath(ns, itemName) {
const idx = textureIndex[ns];
if (!idx) return null;
// Helper: check if a baseName exists and return preferred path
function find(baseName, preferFolder) {
const paths = idx[baseName];
if (!paths || paths.length === 0) return null;
if (preferFolder) {
const match = paths.find(p => p.startsWith(preferFolder + '/'));
if (match) return match;
}
return paths[0];
}
// Helper: check if a baseName with a suffix exists in block/
function findBlock(baseName) {
const suffix = BLOCK_SUFFIXES[baseName];
if (suffix) {
const hit = find(baseName + suffix, 'block') || find(baseName, 'block');
if (hit) return hit;
}
return find(baseName, 'block');
}
// 1. Direct lookup — try item/ then block/
let hit = find(itemName, 'item') || findBlock(itemName);
if (hit) return hit;
// 2. Server-side alias
if (SERVER_ALIASES[itemName]) {
const alias = SERVER_ALIASES[itemName];
hit = find(alias, 'item') || findBlock(alias);
if (hit) return hit;
}
// 3. Smooth blocks
if (SMOOTH_ALIASES[itemName]) {
hit = find(SMOOTH_ALIASES[itemName], 'block');
if (hit) return hit;
}
// 4. Animated items — pick a representative frame
if (ANIMATED_FRAME[itemName]) {
hit = find(ANIMATED_FRAME[itemName], 'item');
if (hit) return hit;
}
// 5. Carpets → wool texture
if (itemName.endsWith('_carpet')) {
const color = itemName.slice(0, -'_carpet'.length);
if (DYE_COLORS.has(color)) {
hit = find(color + '_wool', 'block');
if (hit) return hit;
}
}
// 6. Wood → log texture
if (itemName.endsWith('_wood')) {
const base = itemName.slice(0, -'_wood'.length);
const stripped = base.startsWith('stripped_') ? base.slice('stripped_'.length) : null;
const woodType = stripped || base;
if (WOOD_TYPES.has(woodType)) {
const logName = stripped ? `stripped_${woodType}_log` : `${woodType}_log`;
hit = find(logName, 'block');
if (hit) return hit;
}
}
// 7. Derivative blocks (stairs, slabs, fences, walls…) → parent block
for (const suffix of DERIVATIVE_SUFFIXES) {
if (!itemName.endsWith(suffix)) continue;
const base = itemName.slice(0, -suffix.length);
// Try direct parent
hit = findBlock(base);
if (hit) return hit;
// Wood → planks
if (WOOD_TYPES.has(base)) {
hit = findBlock(base + '_planks');
if (hit) return hit;
}
// Stone aliases (brick → bricks, etc.)
if (STONE_ALIASES[base]) {
hit = findBlock(STONE_ALIASES[base]);
if (hit) return hit;
}
break; // only one derivative suffix can match
}
// 8. Prefix match — for items like "compass" try "compass_*"
const prefixMatches = Object.keys(idx).filter(k => k.startsWith(itemName + '_'));
if (prefixMatches.length > 0) {
// Sort for deterministic result, prefer item/ folder
prefixMatches.sort();
for (const m of prefixMatches) {
hit = find(m, 'item');
if (hit) return hit;
}
hit = find(prefixMatches[0]);
if (hit) return hit;
}
return null;
}
// ── Smart resolution endpoint ─────────────────────────────────────────────
// GET /api/texture/resolve/:namespace/:itemName
// Returns the texture PNG for a Minecraft registry item name, using smart
// matching (aliases, animation frames, carpet→wool, etc.)
app.get('/api/texture/resolve/:namespace/:itemName', (req, res) => {
const { namespace, itemName } = req.params;
const name = itemName.replace(/\.png$/i, '');
const resolved = resolveTexturePath(namespace, name);
if (!resolved) {
return res.status(404).send('No texture found');
}
const fullPath = path.join(TEXTURE_CACHE_DIR, namespace, resolved);
if (!fs.existsSync(fullPath)) {
return res.status(404).send('Texture file missing');
}
res.set('Content-Type', 'image/png');
res.set('Cache-Control', 'public, max-age=604800');
res.set('X-Texture-Resolved', resolved);
return res.sendFile(fullPath);
});
// ── Legacy texture proxy (fallback for cache misses) ──────────────────────
app.get('/api/texture/:namespace/*', async (req, res) => {
const { namespace } = req.params;
const texturePath = req.params[0]; // e.g. "item/diamond.png"
const upstream = TEXTURE_UPSTREAMS[namespace];
const texturePath = req.params[0];
const upstream = UPSTREAM_URLS[namespace];
if (!upstream) {
return res.status(404).send('Unknown namespace');
}