Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Read, write and manipulate minecraft schematics.

Supported formats:
* [Sponge](https://github.com/SpongePowered/Schematic-Specification) (Read/Write)
* [Sponge](https://github.com/SpongePowered/Schematic-Specification) v1-v3 (Read; writes v2)
* [MCEdit](https://minecraft.gamepedia.com/Schematic_file_format) (Read only)

## Usage
Expand Down Expand Up @@ -89,9 +89,10 @@ Static, async. Make a schematic instance from `world` (prismarine-world) between

Async. Paste the schematic in `world` (prismarine-world) at the `at` (vec3) location.

#### Schematic.read(buffer, version=null)
#### Schematic.read(buffer, version=null, format=null)

Static, async. Return a Schematic instance, read from the buffer. If version is not set, the loader try to autodetect the version from the file.
Static, async. Return a Schematic instance read from the buffer. If version is not set, the loader tries to autodetect the version from the file.
The format is detected from the file by default. Set `format` to `mcedit`, `sponge`, `sponge.1`, `sponge.2`, or `sponge.3` to require a particular format.

#### Schematic.write()

Expand Down
4 changes: 3 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Vec3 } from "vec3";
import type { Block } from "prismarine-block";

export type SchematicFormat = "mcedit" | "sponge" | "sponge.1" | "sponge.2" | "sponge.3";

export class Schematic {
public readonly version: string;
public readonly size: Vec3;
Expand Down Expand Up @@ -28,6 +30,6 @@ export class Schematic {
toJSON( space?: string ): string;

static copy( world: any, start: Vec3, end: Vec3, offset: Vec3, version: string ): Promise<Schematic>;
static read( buffer: Buffer, version?: string ): Promise<Schematic>;
static read( buffer: Buffer, version?: string | null, format?: SchematicFormat | null ): Promise<Schematic>;
static fromJSON( string: string ): Schematic;
}
24 changes: 18 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@ const { Vec3 } = require('vec3')
const mcData = require('minecraft-data')

const sponge = require('./lib/spongeSchematic')
const spongeV3 = require('./lib/spongeV3Schematic')
const mcedit = require('./lib/mceditSchematic')

function detectFormat (schem) {
if (Number.isInteger(schem.Schematic?.Version)) return `sponge.${schem.Schematic.Version}`
if (schem.Materials !== undefined && schem.Blocks !== undefined && schem.Data !== undefined) return 'mcedit'
if (Number.isInteger(schem.Version)) return `sponge.${schem.Version}`
throw new Error('The schematic does not match a supported format')
}

class Schematic {
constructor (version, size, offset, palette, blocks) {
this.version = version
Expand Down Expand Up @@ -128,13 +136,17 @@ class Schematic {
}, this.size.z, this.size.x, this.size.y, at.plus(this.start()))
}

static async read (buffer, version = null) {
static async read (buffer, version = null, format = null) {
const schem = nbt.simplify(await parseNbt(buffer))
try {
return sponge.read(schem, version)
} catch {
return mcedit.read(schem, version)
}
const detectedFormat = detectFormat(schem)
if (format === 'sponge') format = detectedFormat.startsWith('sponge.') ? detectedFormat : format
format = format || detectedFormat

if (format !== detectedFormat) throw new Error(`Expected ${format} schematic, found ${detectedFormat}`)
if (format === 'sponge.1' || format === 'sponge.2') return sponge.read(schem, version)
if (format === 'sponge.3') return spongeV3.read(schem, version)
if (format === 'mcedit') return mcedit.read(schem, version)
throw new Error(`Unsupported schematic format ${format}`)
}

async write () {
Expand Down
67 changes: 67 additions & 0 deletions lib/spongeCommon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const { Vec3 } = require('vec3')
const versions = require('minecraft-data').versions.pc
const { parseBlockName, getStateId } = require('./states')

function findVersion (dataVersion) {
const version = versions.find(version => version.dataVersion === dataVersion)
if (!version) throw new Error(`Unsupported Minecraft data version ${dataVersion}; pass a Minecraft version to Schematic.read`)
return version.minecraftVersion
}

function parsePalette (mcData, palette) {
if (!palette || typeof palette !== 'object') throw new Error('Sponge schematic is missing its block palette')

const out = []
const ids = new Set()
for (const [str, id] of Object.entries(palette)) {
if (!Number.isInteger(id) || id < 0) throw new Error(`Invalid palette index for ${str}: ${id}`)
if (ids.has(id)) throw new Error(`Duplicate palette index ${id}`)
ids.add(id)

const { name, properties } = parseBlockName(str)
out[id] = getStateId(mcData, name, properties)
}
return out
}

function readSize (nbt) {
const size = new Vec3(nbt.Width & 0xFFFF, nbt.Height & 0xFFFF, nbt.Length & 0xFFFF)
if (size.x === 0 || size.y === 0 || size.z === 0) throw new Error('Sponge schematic dimensions must be greater than zero')
return size
}

function readOffset (offset) {
if (offset === undefined) return new Vec3(0, 0, 0)
if (!Array.isArray(offset) || offset.length !== 3) throw new Error('Sponge schematic offset must contain three integers')
return new Vec3(offset[0], offset[1], offset[2])
}

function decodeBlockData (byteArray, expectedLength, palette) {
if (!byteArray || typeof byteArray.length !== 'number') throw new Error('Sponge schematic is missing its block data')

const blocks = []
let offset = 0
while (offset < byteArray.length) {
let value = 0
let shift = 0
let byte
do {
if (offset >= byteArray.length) throw new Error('Truncated VarInt in Sponge schematic block data')
byte = byteArray[offset++] & 0xFF
value += (byte & 0x7F) * (2 ** shift)
shift += 7
if (shift > 35 || (shift === 35 && (byte & 0xF8) !== 0)) throw new Error('VarInt is too large in Sponge schematic block data')
} while ((byte & 0x80) !== 0)

if (palette[value] === undefined) throw new Error(`Block data references missing palette index ${value}`)
blocks.push(value)
if (blocks.length > expectedLength) throw new Error(`Sponge schematic has more than ${expectedLength} blocks`)
}

if (blocks.length !== expectedLength) {
throw new Error(`Sponge schematic block count is ${blocks.length}, expected ${expectedLength}`)
}
return blocks
}

module.exports = { findVersion, parsePalette, readSize, readOffset, decodeBlockData }
48 changes: 8 additions & 40 deletions lib/spongeSchematic.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
const { Vec3 } = require('vec3')
const versions = require('minecraft-data').versions.pc
const { parseBlockName, getStateId } = require('./states')

function findVersion (dataVersion) {
for (const v of versions) {
if (v.dataVersion === dataVersion) {
return v.minecraftVersion
}
}
return versions[0].minecraftVersion // default to latest
}
const { findVersion, parsePalette, readSize, readOffset, decodeBlockData } = require('./spongeCommon')

function getDataVersion (mcVersion) {
for (const v of versions) {
Expand All @@ -20,15 +11,6 @@ function getDataVersion (mcVersion) {
return versions[0].dataVersion
}

function parsePalette (mcData, palette) {
const out = []
for (const [str, id] of Object.entries(palette)) {
const { name, properties } = parseBlockName(str)
out[id] = getStateId(mcData, name, properties)
}
return out
}

function writeBlockName (block) {
const prop = []
for (const [key, value] of Object.entries(block.getProperties())) {
Expand All @@ -46,22 +28,6 @@ function writePalette (Block, palette) {
return out
}

function byteArrayToVarintArray (byteArray) {
const varintArray = []
let i = 0
while (i < byteArray.length) {
let value = 0
let varintLength = 0
while (true) {
value |= (byteArray[i] & 127) << (varintLength++ * 7)
if (varintLength > 5) throw new Error('VarInt too big (probably corrupted data)')
if ((byteArray[i++] & 128) !== 128) break
}
varintArray.push(value)
}
return varintArray
}

function varintArrayToByteArray (varintArray) {
const byteArray = []
for (let id of varintArray) {
Expand All @@ -76,16 +42,18 @@ function varintArrayToByteArray (varintArray) {

function read (nbt, version) {
const { Schematic } = require('../')
if (nbt.Version !== 1 && nbt.Version !== 2) throw new Error(`Unsupported Sponge schematic version ${nbt.Version}`)
if (!version) {
version = findVersion(nbt.DataVersion)
version = nbt.Version === 1 ? '1.13.2' : findVersion(nbt.DataVersion)
}
const mcData = require('minecraft-data')(version)
if (!mcData) throw new Error(`Unsupported Minecraft version ${version}`)
const palette = parsePalette(mcData, nbt.Palette)
const size = new Vec3(nbt.Width, nbt.Height, nbt.Length)
const offset = nbt.Metadata && nbt.Metadata.WEOffsetX
const size = readSize(nbt)
const offset = nbt.Metadata && nbt.Metadata.WEOffsetX !== undefined
? new Vec3(nbt.Metadata.WEOffsetX, nbt.Metadata.WEOffsetY, nbt.Metadata.WEOffsetZ)
: new Vec3(0, 0, 0)
const blocks = byteArrayToVarintArray(nbt.BlockData)
: readOffset(nbt.Offset)
const blocks = decodeBlockData(nbt.BlockData, size.x * size.y * size.z, palette)
return new Schematic(version, size, offset, palette, blocks)
}

Expand Down
19 changes: 19 additions & 0 deletions lib/spongeV3Schematic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const { findVersion, parsePalette, readSize, readOffset, decodeBlockData } = require('./spongeCommon')

function read (root, version) {
const { Schematic } = require('../')
const nbt = root.Schematic
if (!nbt || nbt.Version !== 3) throw new Error(`Unsupported Sponge schematic version ${nbt?.Version}`)

version = version || findVersion(nbt.DataVersion)
const mcData = require('minecraft-data')(version)
if (!mcData) throw new Error(`Unsupported Minecraft version ${version}`)

if (!nbt.Blocks) throw new Error('Sponge v3 schematic is missing its Blocks container')
const size = readSize(nbt)
const palette = parsePalette(mcData, nbt.Blocks.Palette)
const blocks = decodeBlockData(nbt.Blocks.Data, size.x * size.y * size.z, palette)
return new Schematic(version, size, readOffset(nbt.Offset), palette, blocks)
}

module.exports = { read }
16 changes: 10 additions & 6 deletions lib/states.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
function parseBlockName (str) {
let name
let prop
if (str.includes('[')) {
let [name, prop] = str.split('[')
[name, prop] = str.split('[', 2)
prop = prop.substring(0, prop.length - 1)
return { name: name.split(':')[1], properties: prop.split(',').map(x => x.split('=')) }
} else {
return { name: str.split(':')[1], properties: [] }
name = str
prop = ''
}

const separator = name.indexOf(':')
if (separator !== -1) name = name.substring(separator + 1)
return { name, properties: prop ? prop.split(',').map(x => x.split('=')) : [] }
}

function parseValue (value, state) {
if (state.type === 'enum') {
return state.values.indexOf(value)
}
if (state.values) return state.values.indexOf(String(value))
if (value === 'true') return 0
if (value === 'false') return 1
return parseInt(value, 10)
Expand Down
Loading
Loading