quake 3 | replace entities in map | draw hints in map | Search

This code analyzes Quake map files to calculate their bounding boxes and potentially adds skyboxes to them.

Run example

npm run import -- "add skybox to map"

add skybox to map

var importer = require('../Core')
var {doIntersect} = importer.import("brush to vertex")

function getBounds(file) {
    // get all brushes in map, leaf nodes with at least one vertex
    var brushes = importer.regexToArray(/\{[\s\S]*?\}/ig, file)
   
    brushes = brushes.map(b => {
        var points = importer
            .regexToArray(/\(((\s*[0-9\.-]+\s*)*)\)/ig, b, 1)
            .map(m => m.trim().split(/\s+/ig)
                .map(n => (n.includes('.')
                    ? parseFloat(n.trim())
                    : parseInt(n.trim()))))
        
        return [[
            Math.min.apply(null, points.map(b => b[0])),
            Math.min.apply(null, points.map(b => b[1])),
            Math.min.apply(null, points.map(b => b[2]))
        ], [
            Math.max.apply(null, points.map(b => b[0])),
            Math.max.apply(null, points.map(b => b[1])),
            Math.max.apply(null, points.map(b => b[2]))
        ]]
    })
 
    // replace all origins with scaled
    // TODO: make this a function
    var origins = importer
        .regexToArray(/"origin"\s+"((\s*[0-9\.-]+\s*)*)"/ig, file, 1)
        .map(o => o.trim().split(/\s+/ig)
            .map(n => (n.includes('.')
                ? parseFloat(n.trim())
                : parseInt(n.trim()))))
    
    origins = origins.concat.apply(origins, brushes)
        .filter(o => o && isFinite(o[0]))
    
    return [[
        Math.min.apply(null, origins.map(b => b[0])),
        Math.min.apply(null, origins.map(b => b[1])),
        Math.min.apply(null, origins.map(b => b[2]))
    ], [
        Math.max.apply(null, origins.map(b => b[0])),
        Math.max.apply(null, origins.map(b => b[1])),
        Math.max.apply(null, origins.map(b => b[2]))
    ]]
}


function addSkybox(fileName) {
    var file
    if(typeof fileName === 'string' && fs.existsSync(fileName)) {
        file = fs.readFileSync(fileName).toString('utf-8')
    } else {
        file = fileName
    }

    var brushes = importer.regexToArray(/\{[^\{}]*?\}\s*/ig, file)
    brushes.forEach(b => {
        if(b.includes('/sky')) {
            file = file.replace(b, '')
            return false
        }
        return true
    })
    
    var vs = getBounds(file)
    
    // TODO: use a fancy for loop instead contains each corner and extends towards the next two points?
    var points = [
        [vs[0][0], vs[0][1], vs[0][2]-16],
        [vs[1][0], vs[1][1], vs[0][2]],
        
        [vs[0][0]-16, vs[0][1], vs[0][2]],
        [vs[0][0],    vs[1][1], vs[1][2]],
        
        [vs[0][0], vs[0][1]-16, vs[0][2]],
        [vs[1][0], vs[0][1],    vs[1][2]],
        
        
        [vs[0][0], vs[0][1], vs[1][2]],
        [vs[1][0], vs[1][1], vs[1][2]+16],
        
        [vs[1][0],    vs[0][1], vs[0][2]],
        [vs[1][0]+16, vs[1][1], vs[1][2]],
        
        [vs[0][0], vs[1][1],    vs[0][2]],
        [vs[1][0], vs[1][1]+16, vs[1][2]],
        
    ]
    var newBrush = ``
    for(var i = 0; i < points.length / 2; i++) {
        var p1 = points[i*2]
        var p2 = points[i*2+1]
        newBrush += `
{ // brush 0
( ${p1[0]} ${p1[1]} ${p2[2]} ) ( ${p1[0]} ${p1[1]} ${p1[2]} ) ( ${p1[0]} ${p2[1]} ${p1[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
( ${p2[0]} ${p2[1]} ${p2[2]} ) ( ${p2[0]} ${p2[1]} ${p1[2]} ) ( ${p2[0]} ${p1[1]} ${p1[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
( ${p2[0]} ${p1[1]} ${p2[2]} ) ( ${p2[0]} ${p1[1]} ${p1[2]} ) ( ${p1[0]} ${p1[1]} ${p1[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
( ${p1[0]} ${p2[1]} ${p2[2]} ) ( ${p1[0]} ${p2[1]} ${p1[2]} ) ( ${p2[0]} ${p2[1]} ${p1[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
( ${p1[0]} ${p2[1]} ${p1[2]} ) ( ${p1[0]} ${p1[1]} ${p1[2]} ) ( ${p2[0]} ${p1[1]} ${p1[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
( ${p1[0]} ${p1[1]} ${p2[2]} ) ( ${p1[0]} ${p2[1]} ${p2[2]} ) ( ${p2[0]} ${p2[1]} ${p2[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
}
`
    }
    

    var exp = (/\{*\s*\/\/\s*brush\s*0\s*\{*/ig)
    var match = exp.exec(file)
    var pos = exp.lastIndex
    file = file.substr(0, pos - match[0].length) + newBrush + file.substr(pos - match[0].length)
    
    if(typeof fileName === 'string' && fs.existsSync(fileName)) {
        console.log(`writing ${fileName}`)
        fs.writeFileSync(fileName, file)
    } else {
        return file
    }
}

module.exports = {
    addSkybox,
    getBounds
}

What the code could have been:

const { importCore } = require('../Core');
const { doIntersect } = importCore.import('brush to vertex');
const fs = require('fs');

const regexToArray = (pattern, string, flags) => {
    const matches = string.match(new RegExp(pattern, flags)) || [];
    return matches.map((match, index) => {
        const groups = match.match(new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\${OUTPUT}amp;'), flags));
        return groups.map((group, index) => group.trim().split(/\s+/ig).map((value) => {
            return value.includes('.')? parseFloat(value.trim()) : parseInt(value.trim());
        }));
    });
};

const getBounds = (file) => {
    const importer = importCore;
    const brushes = importer.regexToArray(/\{[\s\S]*?\}/ig, file);
    const points = brushes.flatMap((brush) => {
        const points = importer.regexToArray(/\(((\s*[0-9\.-]+\s*)*)\)/ig, brush, 1).map((point) => point.trim().split(/\s+/ig).map((value) => {
            return value.includes('.')? parseFloat(value.trim()) : parseInt(value.trim());
        }));
        return points;
    });
    const origins = importer.regexToArray(/"origin"\s+"((\s*[0-9\.-]+\s*)*)"/ig, file, 1).map((origin) => origin.trim().split(/\s+/ig).map((value) => {
        return value.includes('.')? parseFloat(value.trim()) : parseInt(value.trim());
    })).flat();

    const min = [Math.min(...points.map((point) => point[0])), Math.min(...points.map((point) => point[1])), Math.min(...points.map((point) => point[2]))];
    const max = [Math.max(...points.map((point) => point[0])), Math.max(...points.map((point) => point[1])), Math.max(...points.map((point) => point[2]))];

    const allPoints = origins.concat(points);
    const newMin = [Math.min(...allPoints.map((point) => point[0])), Math.min(...allPoints.map((point) => point[1])), Math.min(...allPoints.map((point) => point[2]))];
    const newMax = [Math.max(...allPoints.map((point) => point[0])), Math.max(...allPoints.map((point) => point[1])), Math.max(...allPoints.map((point) => point[2]))];

    return [[newMin[0], newMin[1], newMin[2]], [newMax[0], newMax[1], newMax[2]]];
}

const addSkybox = (fileName) => {
    if (typeof fileName ==='string' && fs.existsSync(fileName)) {
        const file = fs.readFileSync(fileName, 'utf-8');
    } else {
        const file = fileName;
    }

    const brushes = importCore.regexToArray(/\{[^\{}]*?\}\s*/ig, file);
    const skyboxBrushes = brushes.filter((brush) => brush.includes('/sky'));

    skyboxBrushes.forEach((brush) => {
        file = file.replace(brush, '');
    });

    const vs = getBounds(file);

    const points = [
        [vs[0][0], vs[0][1], vs[0][2] - 16],
        [vs[1][0], vs[1][1], vs[0][2]],
        [vs[0][0] - 16, vs[0][1], vs[0][2]],
        [vs[0][0], vs[1][1], vs[1][2]],
        [vs[0][0], vs[0][1] - 16, vs[0][2]],
        [vs[1][0], vs[0][1], vs[1][2]],
        [vs[0][0], vs[0][1], vs[1][2]],
        [vs[1][0], vs[1][1], vs[1][2] + 16],
        [vs[1][0], vs[0][1], vs[0][2]],
        [vs[1][0] + 16, vs[1][1], vs[1][2]],
        [vs[0][0], vs[1][1], vs[0][2]],
        [vs[1][0], vs[1][1] + 16, vs[1][2]],
    ];

    const newBrush = points.flatMap((point, index) => {
        const p1 = points[index];
        const p2 = points[index + 1];
        return `
( ${p1[0]} ${p1[1]} ${p2[2]} ) ( ${p1[0]} ${p1[1]} ${p1[2]} ) ( ${p1[0]} ${p2[1]} ${p1[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
( ${p2[0]} ${p2[1]} ${p2[2]} ) ( ${p2[0]} ${p2[1]} ${p1[2]} ) ( ${p2[0]} ${p1[1]} ${p1[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
( ${p2[0]} ${p1[1]} ${p2[2]} ) ( ${p2[0]} ${p1[1]} ${p1[2]} ) ( ${p1[0]} ${p1[1]} ${p1[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
( ${p1[0]} ${p2[1]} ${p2[2]} ) ( ${p1[0]} ${p2[1]} ${p1[2]} ) ( ${p2[0]} ${p2[1]} ${p1[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
( ${p1[0]} ${p2[1]} ${p1[2]} ) ( ${p1[0]} ${p1[1]} ${p1[2]} ) ( ${p2[0]} ${p1[1]} ${p1[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
( ${p1[0]} ${p1[1]} ${p2[2]} ) ( ${p1[0]} ${p2[1]} ${p2[2]} ) ( ${p2[0]} ${p2[1]} ${p2[2]} ) e1u1/sky1 0 0 0 1 1 0 0 0
`;
    }).join('\n');

    const exp = /\{*\s*\/\/\s*brush\s*0\s*\{*/ig;
    const match = exp.exec(file);
    const pos = exp.lastIndex;
    file = file.substr(0, pos - match[0].length) + newBrush + file.substr(pos - match[0].length);

    if (typeof fileName ==='string' && fs.existsSync(fileName)) {
        console.log(`writing ${fileName}`);
        fs.writeFileSync(fileName, file);
    } else {
        return file;
    }
};

module.exports = {
    addSkybox,
    getBounds
}

This code snippet analyzes a Quake map file to determine its bounding box and potentially adds a skybox.

Here's a breakdown:

  1. getBounds Function:

  2. addSkybox Function:

Purpose:

This code likely serves as a utility for analyzing and potentially modifying Quake map files. The getBounds function provides a way to determine the spatial extent of a map, which can be useful for various purposes such as collision detection or level design analysis. The addSkybox function suggests an intention to automate the addition of skybox elements to maps.