UNPKG

@minecraft/creator-tools

Version:

Minecraft Creator Tools command line and libraries.

327 lines (326 loc) 14.2 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const Structure_1 = __importDefault(require("./Structure")); const BlockVolume_1 = __importDefault(require("./BlockVolume")); /** * Utility functions for working with Structure (MCStructure) objects. */ class StructureUtilities { /** * Infers the size of an IBlockVolume from its data. * - size.y = number of layers in blockLayersBottomToTop * - size.z = maximum number of rows across all layers * - size.x = maximum string length across all rows * * @param blockVolume The IBlockVolume to measure * @returns The inferred size as an IVector3 */ static inferBlockVolumeSize(blockVolume) { const layers = blockVolume.blockLayersBottomToTop; const sizeY = layers.length; let sizeZ = 0; let sizeX = 0; for (const layer of layers) { if (layer.length > sizeZ) { sizeZ = layer.length; } for (const row of layer) { if (row.length > sizeX) { sizeX = row.length; } } } return { x: sizeX, y: sizeY, z: sizeZ }; } /** * Gets the effective size of an IBlockVolume, using the explicit size if provided, * or inferring it from the data if not. * * @param blockVolume The IBlockVolume to get the size of * @returns The effective size as an IVector3 */ static getEffectiveSize(blockVolume) { if (blockVolume.size) { return blockVolume.size; } return StructureUtilities.inferBlockVolumeSize(blockVolume); } /** * Creates a Structure (MCStructure) from an IBlockVolume. * * IBlockVolume uses blockLayersBottomToTop format: * - Outer array: Y layers from bottom (Y=0) to top * - Each layer: rows from north (Z=0) to south (Z=max) * - Each character: X position from west (X=0) to east (X=max) * * Think of it like stacking floors: first layer is ground floor, last is roof. * * If size is not explicitly provided, it is inferred from the data. * Shorter strings and missing rows are treated as air. * * @param blockVolume The IBlockVolume containing layer-based block data * @returns A Structure populated with blocks from the IBlockVolume */ static createStructureFromIBlockVolume(blockVolume) { const structure = new Structure_1.default(); const cube = new BlockVolume_1.default(); // Use explicit size if provided, otherwise infer from data const effectiveSize = StructureUtilities.getEffectiveSize(blockVolume); const sizeX = effectiveSize.x; const sizeY = effectiveSize.y; const sizeZ = effectiveSize.z; cube.setMaxDimensions(sizeX, sizeY, sizeZ); // Set the origin based on the IBlockVolume's southWestBottom structure.originX = blockVolume.southWestBottom.x; structure.originY = blockVolume.southWestBottom.y; structure.originZ = blockVolume.southWestBottom.z; // Process each Y layer (bottom to top) for (let y = 0; y < sizeY && y < blockVolume.blockLayersBottomToTop.length; y++) { const layer = blockVolume.blockLayersBottomToTop[y]; if (!layer) { continue; } // Process each Z row within the layer (north to south) for (let z = 0; z < layer.length && z < sizeZ; z++) { const row = layer[z]; if (!row) { continue; } // Process each X position in the row (west to east) for (let x = 0; x < row.length && x < sizeX; x++) { const charRef = row[x]; const blockTypeData = blockVolume.key[charRef]; if (blockTypeData) { const block = cube.x(x).y(y).z(z); StructureUtilities.applyBlockTypeDataToBlock(block, blockTypeData); } // If no blockTypeData found for the character, the block remains as default (air) } } } structure.cube = cube; return structure; } /** * Applies block type data from IBlockTypeData to a Block. * Handles the properties field and also legacy parsing of block states from the typeId if present. * * @param block The Block to apply the type data to * @param blockTypeData The IBlockTypeData containing the type ID and optional properties */ static applyBlockTypeDataToBlock(block, blockTypeData) { let typeId = blockTypeData.typeId; // First, apply properties from the properties field (preferred method) if (blockTypeData.properties) { for (const propName in blockTypeData.properties) { const prop = block.ensureProperty(propName); prop.value = blockTypeData.properties[propName]; } } // Also support legacy parsing of block states from the typeId // Format: "minecraft:block_name[state1=value1,state2=value2]" or "minecraft:block_name{state1:value1,state2:value2}" const squareBracketIndex = typeId.indexOf("["); const curlyBracketIndex = typeId.indexOf("{"); if (squareBracketIndex >= 0) { // Handle square bracket format: block_name[state=value] const endBracket = typeId.indexOf("]"); if (endBracket > squareBracketIndex) { const statesStr = typeId.substring(squareBracketIndex + 1, endBracket); typeId = typeId.substring(0, squareBracketIndex); StructureUtilities.parseAndApplyBlockStates(block, statesStr, ",", "="); } } else if (curlyBracketIndex >= 0) { // Handle curly bracket format: block_name{state:value} const endBracket = typeId.indexOf("}"); if (endBracket > curlyBracketIndex) { const statesStr = typeId.substring(curlyBracketIndex + 1, endBracket); typeId = typeId.substring(0, curlyBracketIndex); StructureUtilities.parseAndApplyBlockStates(block, statesStr, ",", ":"); } } // Ensure the typeId has the minecraft: prefix if it doesn't have a namespace if (!typeId.includes(":")) { typeId = "minecraft:" + typeId; } block.typeName = typeId; } /** * Parses a block states string and applies the states to the block. * * @param block The block to apply states to * @param statesStr The states string (e.g., "facing=north,half=bottom") * @param stateSeparator The character separating multiple states (usually ",") * @param keyValueSeparator The character separating key from value ("=" or ":") */ static parseAndApplyBlockStates(block, statesStr, stateSeparator, keyValueSeparator) { const states = statesStr.split(stateSeparator); for (const state of states) { const trimmedState = state.trim(); if (!trimmedState) { continue; } const separatorIndex = trimmedState.indexOf(keyValueSeparator); if (separatorIndex > 0) { const key = trimmedState.substring(0, separatorIndex).trim(); const valueStr = trimmedState.substring(separatorIndex + 1).trim(); const prop = block.ensureProperty(key); // Try to parse the value as appropriate type if (valueStr === "true") { prop.value = true; } else if (valueStr === "false") { prop.value = false; } else { const numValue = parseInt(valueStr, 10); if (!isNaN(numValue) && numValue.toString() === valueStr) { prop.value = numValue; } else { // Remove quotes if present prop.value = valueStr.replace(/^["']|["']$/g, ""); } } } } } /** * Creates an IBlockVolume from a Structure. * This is the inverse operation of createStructureFromIBlockVolume. * * Output uses blockLayersBottomToTop format: * - Outer array: Y layers from bottom to top * - Each layer: rows from north to south * - Each character: X position from west to east * * @param structure The Structure to convert * @returns An IBlockVolume representation of the structure */ static createIBlockVolumeFromStructure(structure) { const cube = structure.cube; if (!cube) { return undefined; } const key = {}; const blockLayersBottomToTop = []; const blockTypeToChar = new Map(); // Character set for block references (avoiding space which is used for air) const charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+=[]{}|;':\",./<>?`~"; let charIndex = 0; // Process each Y layer (bottom to top) for (let y = 0; y < cube.maxY; y++) { const layer = []; // Process each Z row within the layer (north to south) for (let z = 0; z < cube.maxZ; z++) { let row = ""; // Process each X position (west to east) for (let x = 0; x < cube.maxX; x++) { const block = cube.x(x).y(y).z(z); const blockFingerprint = block.toString(); // Check for air blocks if (block.typeName === null || block.typeName === undefined || block.shortTypeId === "air") { row += " "; continue; } // Look up or assign a character for this block type let charRef = blockTypeToChar.get(blockFingerprint); if (charRef === undefined) { if (charIndex >= charSet.length) { // If we run out of characters, start using multi-character references // This is a fallback that shouldn't happen often charRef = `{${charIndex}}`; } else { charRef = charSet[charIndex]; } charIndex++; blockTypeToChar.set(blockFingerprint, charRef); key[charRef] = StructureUtilities.getBlockTypeDataFromBlock(block); } row += charRef; } layer.push(row); } blockLayersBottomToTop.push(layer); } return { entities: [], // TODO: Support entity conversion southWestBottom: { x: structure.originX ?? 0, y: structure.originY ?? 0, z: structure.originZ ?? 0, }, size: { x: cube.maxX, y: cube.maxY, z: cube.maxZ, }, blockLayersBottomToTop, key, }; } /** * Creates an IBlockTypeData from a Block, with typeId and properties separated. * * @param block The block to get the type data for * @returns An IBlockTypeData with the block's type ID and properties */ static getBlockTypeDataFromBlock(block) { const typeId = block.typeName ?? "minecraft:air"; const propertyNames = Object.keys(block.properties); if (propertyNames.length === 0) { return { typeId }; } const properties = {}; for (const propName of propertyNames) { const prop = block.getProperty(propName); if (prop && prop.value !== undefined) { const value = prop.value; // Convert to compatible types for IBlockTypeData.properties if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { properties[propName] = value; } else if (typeof value === "bigint") { // Convert bigint to number (may lose precision for very large values) properties[propName] = Number(value); } else if (Array.isArray(value)) { // Convert arrays to string representation properties[propName] = value.join(","); } } } return { typeId, properties }; } /** * Gets the full block type ID with states in square bracket notation. * @deprecated Use getBlockTypeDataFromBlock instead for the new properties-based format. * * @param block The block to get the type ID for * @returns The block type ID, optionally with states (e.g., "minecraft:oak_stairs[facing=north,half=bottom]") */ static getBlockTypeIdWithStates(block) { let typeId = block.typeName ?? "minecraft:air"; const propertyNames = Object.keys(block.properties); if (propertyNames.length > 0) { const states = []; for (const propName of propertyNames) { const prop = block.getProperty(propName); if (prop && prop.value !== undefined) { states.push(`${propName}=${prop.value}`); } } if (states.length > 0) { typeId += `[${states.join(",")}]`; } } return typeId; } } exports.default = StructureUtilities;