diff --git a/src/grabcraft.js b/src/grabcraft.js index 924e274..bae3729 100644 --- a/src/grabcraft.js +++ b/src/grabcraft.js @@ -125,7 +125,21 @@ export async function fetchBlueprint(url) { : 'Unknown Blueprint'; // Extract render object (3D voxel data) - const voxels = extractRenderObject(html); + // Try external JS file first (new GrabCraft format), then fall back to inline extraction + let voxels = []; + const externalScript = extractExternalScriptUrl(html); + if (externalScript) { + try { + log(TAG, `Fetching external voxel data: ${externalScript.url}`); + const jsContent = await fetchPage(externalScript.url); + voxels = parseExternalRenderObject(jsContent); + } catch (err) { + logError(TAG, `Failed to fetch external JS: ${err.message}`); + } + } + if (voxels.length === 0) { + voxels = extractRenderObject(html); // fallback for legacy pages + } // Extract materials list const materials = extractMaterials(html); @@ -302,6 +316,76 @@ function extractRenderObject(html) { return voxels; } +/** + * Extract URL of external myRenderObject JS file from the page HTML. + * GrabCraft now loads voxel data from e.g. /js/RenderObject/myRenderObject_1234.js + * @param {string} html + * @returns {{ url: string, blueprintId: string } | null} + */ +function extractExternalScriptUrl(html) { + const regex = /]+src=["']((?:https?:\/\/[^"']*)?\/js\/RenderObject\/myRenderObject_(\d+)\.js)["']/i; + const match = html.match(regex); + if (!match) return null; + + let url = match[1]; + if (url.startsWith('/')) { + url = GRABCRAFT_BASE + url; + } + return { url, blueprintId: match[2] }; +} + +/** + * Parse a nested myRenderObject from an external JS file. + * Expected format: var myRenderObject = { y: { x: { z: { mat_id, hex, name, ... } } } }; + * @param {string} jsContent - Raw JS file content + * @returns {Array<{ x: number, y: number, z: number, matId: string, hex: string|null, matName: string|null }>} + */ +function parseExternalRenderObject(jsContent) { + const voxels = []; + + // Strip "var myRenderObject = " prefix and trailing ";" + const stripped = jsContent + .replace(/^\s*var\s+myRenderObject\s*=\s*/, '') + .replace(/;\s*$/, '') + .trim(); + + let obj; + try { + obj = JSON.parse(stripped); + } catch { + logError(TAG, 'Failed to JSON.parse external myRenderObject'); + return voxels; + } + + if (typeof obj !== 'object' || obj === null) return voxels; + + for (const [yKey, yVal] of Object.entries(obj)) { + const y = parseInt(yKey, 10); + if (isNaN(y) || typeof yVal !== 'object' || yVal === null) continue; + + for (const [xKey, xVal] of Object.entries(yVal)) { + const x = parseInt(xKey, 10); + if (isNaN(x) || typeof xVal !== 'object' || xVal === null) continue; + + for (const [zKey, entry] of Object.entries(xVal)) { + const z = parseInt(zKey, 10); + if (isNaN(z) || typeof entry !== 'object' || entry === null) continue; + + voxels.push({ + x, + y, + z, + matId: String(entry.mat_id || '0'), + hex: entry.hex || null, + matName: entry.name || null, + }); + } + } + } + + return voxels; +} + /** * Extract materials list from Highcharts series data. * GrabCraft pages include chart data like: @@ -351,10 +435,13 @@ function extractMaterials(html) { * @returns {Promise} HTML content */ async function fetchPage(url) { + const isJs = url.endsWith('.js'); const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT, - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept': isJs + ? 'application/javascript, */*;q=0.8' + : 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', }, });