UNPKG

@kleppe/litematic-reader

Version:

Example: ```ts import { readFile } from "fs/promises"; import { Litematic } from '@kleppe/litematic-reader'

292 lines 10.6 kB
import { Nbt } from "./nbt"; import { Virtual3DCanvas } from "./virtual_canvas"; import { expandLongPackedArray } from "./long_packed_array"; import { compress } from "./compression"; export const SCHEMATIC_SHAPE = { 'Version': 'int', 'MinecraftDataVersion': 'int', 'Metadata': { 'Name': 'string', 'Author': 'string', 'Description': 'string', 'EnclosingSize': { 'x': 'int', 'y': 'int', 'z': 'int' }, 'TimeCreated': 'long', 'TimeModified': 'long', 'TotalBlocks': 'int', 'TotalVolume': 'int', 'RegionCount': 'int', }, 'Regions': { '*': { 'BlockStatePalette': [{ 'Name': 'string', 'Properties': { '*': 'string' } }], 'BlockStates': 'longArray', 'Position': { 'x': 'int', 'y': 'int', 'z': 'int' }, 'Size': { 'x': 'int', 'y': 'int', 'z': 'int' }, 'Entities': [{ '*': '*' }], 'TileEntities': [{ '*': '*' }], 'PendingBlockTicks': [{ '*': '*' }], } } }; const REGION_SHAPE = SCHEMATIC_SHAPE['Regions']['*']; const BLOCK_STATE_SHAPE = REGION_SHAPE['BlockStatePalette'][0]; /** * Converts the Nbt form of a block state palette entry into * a string like "minecraft:observer[facing=east]". */ export function blockState(state) { if (state['Properties'] != null && Object.keys(state['Properties']).length) { return `${state['Name']}[${Object.keys(state['Properties']) .sort() .map(prop => `${prop}=${state['Properties'][prop]}`) .join(',')}]`; } return `${state['Name']}`; } /** * Parses a string like "minecraft:observer[facing=east]" to * {Name: "minecraft:observer", Properties: {facing: "east"}}. */ export function parseBlockState(state) { const [name, props] = state.split('['); const properties = {}; if (props) { for (const kv of props.slice(0, -1).split(',')) { const [key, value] = kv.split('='); properties[key] = value; } } return { 'Name': name, 'Properties': properties }; } /** * Returns the number of bits needed to represent all of the block states * in nPaletteEntries. There is always a minimum of 2 bits. */ function bitsForBlockStates(nPaletteEntries) { return Math.max(Math.ceil(Math.log2(nPaletteEntries)), 2); } /** * Keeps track of the palette assignments. */ export class PaletteManager { constructor(empty = 'minecraft:air') { this.palette = { [empty]: 0 }; this.paletteList = [empty]; } getOrCreatePaletteIndex(blockState) { if (this.palette[blockState] !== undefined) { return this.palette[blockState]; } this.paletteList.push(blockState); this.palette[blockState] = this.paletteList.length - 1; return this.palette[blockState]; } getBlockState(n) { return this.paletteList[n] ?? 'minecraft:air'; } bits() { return bitsForBlockStates(this.paletteList.length); } toNbt() { return this.paletteList.map(parseBlockState); } } /** * Reads a schematic. */ export class SchematicReader { constructor(fileContents) { this.nbt = new Nbt(SCHEMATIC_SHAPE); this.palette = new PaletteManager(); this.blocks = new Virtual3DCanvas(); this.nbtData = this.nbt.parse(fileContents); const regions = Object.keys(this.nbtData['Regions']); for (const regionName of regions) { const region = this.nbtData['Regions'][regionName]; const palette = region['BlockStatePalette'].map(blockState); const bits = bitsForBlockStates(palette.length); // A region is defined in the UI as two corner points, // but is saved as point 1 (position) and a size. If // the size is negative in an axis, then it indicates // that point 2 is less in that axis, so we adjust to // get the starting point of the blockstates array, which // is always to lower of the two points. const width = region['Size']['x']; const height = region['Size']['y']; const length = region['Size']['z']; const rx = region['Position']['x'] + (width < 0 ? width + 1 : 0); const ry = region['Position']['y'] + (height < 0 ? height + 1 : 0); const rz = region['Position']['z'] + (length < 0 ? length + 1 : 0); const blocks = expandLongPackedArray(region['BlockStates'], bits, Math.abs(width * height * length), true); // Copy the data onto the 3d canvas with the combined palette. for (let y = 0, i = 0; y < Math.abs(height); y++) { for (let z = 0; z < Math.abs(length); z++) { for (let x = 0; x < Math.abs(width); x++, i++) { const block = blocks[i]; const paletteIndex = this.palette.getOrCreatePaletteIndex(palette[block]); this.blocks.set(rx + x, ry + y, rz + z, paletteIndex); } } } } } getBlock(x, y, z) { if (x < 0 || x >= this.width || y < 0 || y >= this.height || z < 0 || z >= this.length) { return 'minecraft:air'; } return this.palette.getBlockState(this.blocks.get(this.blocks.minx + x, this.blocks.miny + y, this.blocks.minz + z)); } get version() { return this.nbtData['Version']; } get minecraftDataVersion() { return this.nbtData['MinecraftDataVersion']; } get name() { return this.nbtData['Metadata']['Name']; } get author() { return this.nbtData['Metadata']['Author']; } get description() { return this.nbtData['Metadata']['Description']; } get totalBlocks() { return this.nbtData['Metadata']['TotalBlocks']; } get totalVolume() { return this.nbtData['Metadata']['TotalVolume']; } get enclosingSize() { return this.nbtData['Metadata']['EnclosingSize']; } get width() { return this.blocks.width; } get height() { return this.blocks.height; } get length() { return this.blocks.length; } get timeCreated() { return this.nbtData['Metadata']['TimeCreated']; } get timeModified() { return this.nbtData['Metadata']['TimeModified']; } } /** * Simple interface for writing schematics with a * single region. Uses a virtual infinite space for * setting blocks, and then uses the smallest bounding box * when saving. */ export class SchematicWriter { constructor(name, author) { this.name = name; this.author = author; this.nbt = new Nbt(SCHEMATIC_SHAPE); this.description = ''; this.paletteManager = new PaletteManager(); this.canvas = new Virtual3DCanvas(); this.version = 5; this.minecraftDataVersion = 2730; } /** * Gets the index of the given block state in the palette, * adding it to the palette if necessary. */ getOrCreatePaletteIndex(blockState) { return this.paletteManager.getOrCreatePaletteIndex(blockState); } /** * Sets the block (x, y, z) to the given block state. */ setBlock(x, y, z, blockState) { this.canvas.set(x, y, z, this.getOrCreatePaletteIndex(blockState)); } /** * Gets the block state at (x, y, z) */ getBlock(x, y, z) { return this.paletteManager.getBlockState(this.canvas.get(x, y, z)); } asNbtData() { const [blocks, nonAirBlocks] = this.canvas.getAllBlocks(); const bits = this.paletteManager.bits(); const uint64sRequired = Math.ceil(blocks.length * bits / 64); const blockStates = new DataView(new ArrayBuffer(uint64sRequired * 8)); // Pack the blocks into the blockStates array using // only the number of bits required for each entry. let current = 0n; let next = 0n; let bitOffset = 0; let blockStatesIndex = 0; for (const block of blocks) { const shifted = BigInt(block) << BigInt(bitOffset); current |= shifted; next |= shifted >> 64n; bitOffset += bits; if (bitOffset >= 64) { bitOffset -= 64; blockStates.setBigUint64(blockStatesIndex, current); blockStatesIndex += 8; current = next; next = 0n; } } const now = BigInt(Date.now()); return { 'Version': this.version, 'MinecraftDataVersion': this.minecraftDataVersion, 'Metadata': { 'Name': this.name, 'Author': this.author, 'Description': this.description, 'TimeCreated': now, 'TimeModified': now, 'TotalBlocks': nonAirBlocks, 'TotalVolume': this.canvas.width * this.canvas.height * this.canvas.length, 'RegionCount': 1, 'EnclosingSize': { 'x': this.canvas.width, 'y': this.canvas.height, 'z': this.canvas.length } }, 'Regions': { [this.name]: { 'BlockStatePalette': this.paletteManager.toNbt(), 'BlockStates': blockStates, 'Position': { 'x': this.canvas.minx, 'y': this.canvas.miny, 'z': this.canvas.minz }, 'Size': { 'x': this.canvas.width, 'y': this.canvas.height, 'z': this.canvas.length }, 'Entities': [], 'PendingBlockTicks': [], 'TileEntities': [], } } }; } async save() { const uncompressed = this.nbt.serialize(this.asNbtData()); return compress(uncompressed); } } //# sourceMappingURL=litematic.js.map