// packs_preview.js — opens a model in the vendored Blockbench app inside a // full-screen overlay. The geo JSON is loaded via the gated pack_asset // endpoint and the texture images are resolved from afw_bone_textures (or the // vanilla entity skin fallback) and handed to Blockbench; the bundled // GeckoLib + Multi Actor Animator plugins handle applying them. (function () { let overlay = null; let iframe = null; let busy = false; const BB_URL = '/static/vendor/blockbench/index.html'; function ensureOverlay() { if (overlay) return overlay; overlay = document.createElement('div'); overlay.className = 'bb-overlay'; overlay.innerHTML = '
' + '' + '' + '
' + '
'; overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); overlay.querySelector('#bb-overlay-close').addEventListener('click', close); document.body.appendChild(overlay); return overlay; } function setStatus(text) { const el = document.querySelector('#bb-overlay-status'); if (el) el.textContent = text || ''; } function close() { if (!overlay) return; overlay.hidden = true; if (iframe) { iframe.remove(); iframe = null; } } function assetUrl(baseUrl, member) { return baseUrl.replace('ASSET', member); } function entityNameFromMember(member) { let base = member.slice(member.lastIndexOf('/') + 1); if (base.endsWith('.geo.json')) base = base.slice(0, -'.geo.json'.length); for (const s of ['.mf', '.fm', '.m', '.f', '.g']) { if (base.endsWith(s)) { base = base.slice(0, -s.length); break; } } return base; } function resourceToMember(resource) { resource = String(resource); if (resource.indexOf(':') !== -1) { const p = resource.split(':'); return 'assets/' + p.shift() + '/' + p.join(':'); } if (resource.startsWith('assets/')) return resource; return 'assets/' + resource; } function loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error('Could not load ' + url)); img.src = url; }); } function imageDataUrl(img) { const c = document.createElement('canvas'); c.width = img.naturalWidth; c.height = img.naturalHeight; c.getContext('2d').drawImage(img, 0, 0); return c.toDataURL('image/png'); } async function fetchGeo(member, baseUrl) { const r = await fetch(assetUrl(baseUrl, member), { headers: { 'Accept': 'application/json' } }); if (!r.ok) throw new Error('Could not load the model file'); return r.json(); } async function fetchVanillaIndex(vanillaBaseUrl) { try { const r = await fetch(assetUrl(vanillaBaseUrl, 'entity_index.json')); return r.ok ? await r.json() : {}; } catch (e) { return {}; } } // Resolve the textures to hand to Blockbench: the pack's _features // overlay(s) plus the vanilla entity skin (a random fur/colour variant when // the mob has several). Blockbench + the bundled plugins UV-wrap and apply // them, so we don't worry about per-bone assignment here. async function resolveTextures(geo, entity, opts, index) { const featureTextures = []; const baseTextures = []; const afw = geo['afw_bone_textures'] || {}; // 1. Feature overlays — from afw_bone_textures, or the conventional // assets//textures/entity//_features.png path. const members = []; let namespace = 'needsofnature'; for (const k in afw) { const m = resourceToMember(afw[k]); if (members.indexOf(m) === -1) members.push(m); const colon = String(afw[k]).indexOf(':'); if (colon > 0) namespace = String(afw[k]).slice(0, colon); } if (!members.length && entity) { members.push('assets/' + namespace + '/textures/entity/' + entity + '/' + entity + '_features.png'); } for (const m of members) { try { const img = await loadImage(assetUrl(opts.baseUrl, m)); featureTextures.push({ name: m.split('/').pop(), dataUrl: imageDataUrl(img) }); } catch (e) { console.warn('packs: skip feature texture', m, e); } } // 2. Vanilla base skin — a random variant when several exist, else the // server-matched pack skin, else none. let rel = null; const ent = index && index[entity]; if (ent) { const variants = ent.variants || (typeof ent === 'string' ? [ent] : [ent.default]); if (variants && variants.length) rel = variants[Math.floor(Math.random() * variants.length)]; } if (rel) { try { const img = await loadImage(assetUrl(opts.vanillaBaseUrl, 'entity/' + rel)); baseTextures.push({ name: rel.split('/').pop(), dataUrl: imageDataUrl(img) }); } catch (e) { console.warn('packs: skip vanilla skin', e); } } else if (opts.defaultTexture) { try { const img = await loadImage(assetUrl(opts.baseUrl, opts.defaultTexture)); baseTextures.push({ name: opts.defaultTexture.split('/').pop(), dataUrl: imageDataUrl(img) }); } catch (e) { console.warn('packs: skip default texture', e); } } // The vanilla/base texture must always be loaded first, then features. // Wide player models additionally always load L1Z0's custom skins // alongside the default Steve skin (tracked in static/player_skins/). if (entity === 'player') { for (const name of ['destroyed_skin.png', 'russian_goat.png']) { try { const img = await loadImage('/static/player_skins/' + name); baseTextures.push({ name: name, dataUrl: imageDataUrl(img) }); } catch (e) { console.warn('packs: skip player skin', name, e); } } } return baseTextures.concat(featureTextures); } async function open(btn) { const member = btn.dataset.member; const name = btn.dataset.name || member; const opts = { baseUrl: btn.dataset.baseUrl, vanillaBaseUrl: btn.dataset.vanillaBaseUrl, defaultTexture: btn.dataset.defaultTexture || null }; if (!member || !opts.baseUrl || busy) return; busy = true; const overlayEl = ensureOverlay(); overlayEl.hidden = false; document.querySelector('#bb-overlay-title').textContent = name; setStatus('Starting Blockbench…'); if (iframe) iframe.remove(); iframe = document.createElement('iframe'); iframe.className = 'bb-frame'; iframe.src = BB_URL; overlayEl.appendChild(iframe); const ready = await new Promise((resolve) => { let done = false; const finish = (ok) => { if (done) return; done = true; window.removeEventListener('message', onMsg); clearTimeout(timer); resolve(ok); }; const timer = setTimeout(() => finish(false), 45000); const onMsg = (e) => { if (e.origin !== window.location.origin) return; if (e.data && e.data.type === 'packs-bb-ready') finish(true); }; window.addEventListener('message', onMsg); }); if (!ready) { setStatus('Blockbench failed to start.'); busy = false; return; } setStatus('Loading model…'); try { const [geo, index] = await Promise.all([ fetchGeo(member, opts.baseUrl), fetchVanillaIndex(opts.vanillaBaseUrl), ]); const entity = entityNameFromMember(member); const textures = await resolveTextures(geo, entity, opts, index); iframe.contentWindow.postMessage({ type: 'packs-open-model', geo: geo, name: name, textures: textures, }, window.location.origin); setStatus('Applying textures…'); } catch (e) { setStatus('Could not load model: ' + e.message); } busy = false; } window.addEventListener('message', (e) => { if (e.origin !== window.location.origin) return; if (!e.data) return; if (e.data.type === 'packs-model-open') { setStatus('Ready — drag to orbit, scroll to zoom.'); } else if (e.data.type === 'packs-model-error') { setStatus('Model error: ' + (e.data.error || '')); } else if (e.data.type === 'packs-plugin-error') { setStatus('Plugin error: ' + (e.data.error || '')); } }); window.PacksPreview = { open: open, close: close }; })();