Reference Source

src/model/modelConverter.js

import * as pako from "pako";
import * as NBT from "prismarine-nbt";
import SkinRender from "../skin/index";
import { loadBlockState } from "../functions";

/**
 * Helper to convert multi-block structures to models used by {@link ModelRender}
 * @constructor
 */
function ModelConverter() {
}

/**
 * Converts a {@link https://minecraft.gamepedia.com/Structure_block_file_format|Minecraft structure file} to models
 * @param {object} structure structure file info
 * @param {string} structure.url URL to a structure file
 * @param {(Blob|File)} structure.file uploaded file
 * @param {(Uint8Array|ArrayBuffer)} structure.raw Raw NBT data
 * @param cb
 */
ModelConverter.prototype.structureToModels = function (structure, cb) {
    loadNBT(structure).then((rawNbt) => {
        NBT.parse(rawNbt, (err, data) => {
            if (err) {
                console.warn("Error while parsing NBT data");
                console.warn(err);
                return;
            }

            if (!PRODUCTION) {
                console.log("NBT Data:")
                console.log(data);
            }

            parseStructureData(data).then((data) => {
                cb(data);
            })
        })
    })
};


/**
 * Converts a Minecraft schematic file to models
 * @param {object} schematic structure file info
 * @param {string} schematic.url URL to a structure file
 * @param {(Blob|File)} schematic.file uploaded file
 * @param {(Uint8Array|ArrayBuffer)} schematic.raw Raw NBT data
 * @param cb
 */
ModelConverter.prototype.schematicToModels = function (schematic, cb) {
    loadNBT(schematic).then(rawNbt => {
        NBT.parse(rawNbt, (err, data) => {
            if (err) {
                console.warn("Error while parsing NBT data");
                console.warn(err);
                return;
            }

            if (!PRODUCTION) {
                console.log("NBT Data:")
                console.log(data);
            }

            let xhr = new XMLHttpRequest();
            xhr.open('GET', "https://minerender.org/res/idsToNames.json", true);
            xhr.onloadend = function () {
                if (xhr.status === 200) {
                    console.log(xhr.response || xhr.responseText);

                    let idsToNames = JSON.parse(xhr.response || xhr.responseText);
                    parseSchematicData(data, idsToNames).then(data => cb(data));
                }
            };
            xhr.send();

        })
    })
};


function loadNBT(source) {
    return new Promise((resolve, reject) => {
        if (source.file) {
            let reader = new FileReader();
            reader.onload = function () {
                let arrayBuffer = this.result;
                let array = new Uint8Array(arrayBuffer);

                resolve(array);
            }
            reader.readAsArrayBuffer(source.file);
        } else if (source.url) {
            let xhr = new XMLHttpRequest();
            xhr.open('GET', source.url, true);
            xhr.responseType = 'arraybuffer';
            xhr.onloadend = function () {
                if (xhr.status === 200) {
                    let array = new Uint8Array(xhr.response || xhr.responseText);

                    resolve(array);
                }
            };
            xhr.send();
        } else if (source.raw) {
            if (source.raw instanceof Uint8Array) {
                resolve(source.raw)
            } else {
                resolve(new Uint8Array(source.raw));
            }
        } else {
            reject();
        }
    })
}

function parseStructureData(data, paletteIndex) {
    return new Promise((resolve, reject) => {
        if (data.type === "compound") {
            if (data.value.hasOwnProperty("blocks") && (data.value.hasOwnProperty("palette") || data.value.hasOwnProperty("palettes"))) {
                let originalPalette;
                if (data.value.hasOwnProperty("palette")) {
                    originalPalette = data.value["palette"].value.value;
                } else {
                    if (typeof paletteIndex === "undefined") paletteIndex = 0;
                    if (paletteIndex >= data.value["palettes"].value.value.length || !data.value["palettes"].value.value[paletteIndex]) {
                        console.warn("Specified palette index (" + paletteIndex + ") is outside of available palettes (" + data.value["palettes"].value.value.length + ")")
                        return;
                    }
                    originalPalette = data.value["palettes"].value.value[paletteIndex].value;
                }


                // Simplify palette
                let palette = [];
                for (let i = 0; i < originalPalette.length; i++) {
                    palette.push(originalPalette[i]);
                }

                let arr = [];

                // Iterate blocks
                let blocks = data.value.blocks.value.value;
                for (let i = 0; i < blocks.length; i++) {
                    let blockType = palette[blocks[i].state.value].Name.value;
                    if (blockType === "minecraft:air") {
                        // No need to add air
                        continue;
                    }
                    let shortBlockType = blockType.substr("minecraft:".length);

                    let pos = blocks[i].pos.value.value;

                    let multipartConditions = {};

                    let variantString = "";
                    if (palette[blocks[i].state.value].hasOwnProperty("Properties")) {
                        let strs = [];
                        for (let p in  palette[blocks[i].state.value].Properties.value) {
                            if (palette[blocks[i].state.value].Properties.value.hasOwnProperty(p)) {
                                let prop = palette[blocks[i].state.value].Properties.value[p];

                                strs.push(p + "=" + prop.value);

                                multipartConditions[p] = prop.value;
                            }
                        }

                        // Make sure the variants are sorted properly, or it won't match the game files
                        strs.sort();

                        for (let i = 0; i < strs.length; i++) {
                            variantString += "," + strs[i];
                        }

                        variantString = variantString.substr(1);
                    }

                    if (specialVariants.hasOwnProperty(shortBlockType)) {
                        shortBlockType = specialVariants[shortBlockType](palette[blocks[i].state.value].Properties.value);
                        variantString = "";
                    }

                    let block = {
                        blockstate: shortBlockType,
                        variant: variantString,
                        multipart: multipartConditions,
                        offset: [pos[0] * 16, pos[1] * 16, pos[2] * 16]
                    };
                    arr.push(block)
                }

                resolve(arr);
            } else {
                console.warn("Invalid NBT - Missing blocks/palette(s)");
                reject();
            }
        } else {
            console.warn("Invalid NBT - Root tag should be compound");
            reject();
        }
    })
}

function parseSchematicData(data, idToNameMap) {
    return new Promise((resolve, reject) => {
        let width = data.value.Width.value;
        let height = data.value.Height.value;
        let length = data.value.Length.value;

        let infoAt = function (x, y, z) {
            let index = (y * length + z) * width + x;
            return {
                id: data.value.Blocks.value[index] & 0xff,
                data: data.value.Data.value[index]
            }
        };

        let convertLegacy = function (id, data) {
            let mapped = idToNameMap.blocks[id + ":" + data];
            if (!mapped) {
                console.warn("Missing legacy mapping for " + id + ":" + data);
                return "minecraft:air";
            }
            return mapped;
        };

        let arr = [];

        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                for (let z = 0; z < length; z++) {
                    let info = infoAt(x, y, z);
                    let convertedInfo = convertLegacy(info.id, info.data);

                    let infoSplit = convertedInfo.replace("minecraft:", "").replace("]", "").split("[");
                    let shortName = infoSplit[0];
                    let variantString = infoSplit[1] || "";

                    if (shortName === "air") continue;

                    if (variantString !== "") {
                        variantString = variantString.split(",").sort().join(",");
                    }

                    arr.push({
                        blockstate: shortName,
                        variant: variantString,
                        offset: [x * 16, y * 16, z * 16]
                    });
                }
            }
        }

        resolve(arr);
    })
}

let specialVariants = {
    "stained_glass": function (properties) {
        return properties.color.value + "_stained_glass";
    },
    "planks": function (properties) {
        return properties.variant.value + "_planks";
    }
};


ModelConverter.prototype.constructor = ModelConverter;

window.ModelConverter = ModelConverter;

export default ModelConverter;