fix(grabcraft): support external JS files for voxel data extraction
All checks were successful
Deploy to Docker / deploy (push) Successful in 13s

GrabCraft moved myRenderObject data from inline scripts to external JS
files (/js/RenderObject/myRenderObject_XXXX.js). This adds extraction of
the external script URL, fetching and parsing the nested JSON object, with
fallback to the original inline extraction for legacy pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 22:45:50 +00:00
parent 6a22a5155b
commit f9e2b43d2b

View File

@@ -125,7 +125,21 @@ export async function fetchBlueprint(url) {
: 'Unknown Blueprint'; : 'Unknown Blueprint';
// Extract render object (3D voxel data) // 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 // Extract materials list
const materials = extractMaterials(html); const materials = extractMaterials(html);
@@ -302,6 +316,76 @@ function extractRenderObject(html) {
return voxels; 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 = /<script[^>]+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. * Extract materials list from Highcharts series data.
* GrabCraft pages include chart data like: * GrabCraft pages include chart data like:
@@ -351,10 +435,13 @@ function extractMaterials(html) {
* @returns {Promise<string>} HTML content * @returns {Promise<string>} HTML content
*/ */
async function fetchPage(url) { async function fetchPage(url) {
const isJs = url.endsWith('.js');
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
'User-Agent': USER_AGENT, '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', 'Accept-Language': 'en-US,en;q=0.5',
}, },
}); });