fix(grabcraft): support external JS files for voxel data extraction
All checks were successful
Deploy to Docker / deploy (push) Successful in 13s
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:
@@ -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',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user