prismarine-chunk
Version:
A class to hold chunk data for prismarine
366 lines (317 loc) • 11.8 kB
JavaScript
const PalettedStorage = require('../common/PalettedStorage')
const { StorageType } = require('../common/constants')
const { getChecksum } = require('../common/util')
const neededBits = require('../../pc/common/neededBits')
const Stream = require('../common/Stream')
const nbt = require('prismarine-nbt')
class SubChunk {
constructor (registry, Block, options = {}) {
this.registry = registry
if (!registry) {
throw new Error('registry is required')
}
this.Block = Block
this.y = options.y
this.palette = options.palette || []
this.blocks = []
if (options.blocks) {
for (const block of options.blocks) {
this.blocks.push(PalettedStorage.copyFrom(block))
}
}
this.subChunkVersion = options.subChunkVersion || 8
this.hash = options.hash || null
this.updated = options.updated || true
// Not written or read
this.blockLight = options.blockLight ? PalettedStorage.copyFrom(options.blockLight) : new PalettedStorage(4)
this.skyLight = options.skyLight ? PalettedStorage.copyFrom(options.skyLight) : new PalettedStorage(4)
}
// Creates an air chunk
static create (registry, Block, y) {
const subChunk = new this(registry, Block, { y })
// Fill first layer with zero
subChunk.blocks.push(new PalettedStorage(1))
subChunk.palette.push([])
// Set zero to be air, Add to the palette
subChunk.addToPalette(0, subChunk.registry.blocksByName.air.defaultState, 4096)
return subChunk
}
decode (format, stream) {
if (stream instanceof Buffer) stream = new Stream(stream)
// version
this.subChunkVersion = stream.readByte()
let storageCount = 1
switch (this.subChunkVersion) {
case 1:
// This is a old SubChunk format that only has one layer - no need to read storage count
// But when re-encoding, we want to use v8 to not loose data
this.subChunkVersion = 8
break
case 8:
case 9:
storageCount = stream.readByte()
if (this.subChunkVersion >= 9) {
this.y = stream.readByte() // Sub Chunk Index
}
if (storageCount > 2) {
// This is technically not an error, but not currently aware of any servers
// that send more than two layers. If this is a problem, this check can be
// safely removed. Just keeping it here as a sanity check.
throw new Error('Expected storage count to be 1 or 2, got ' + storageCount)
}
break
default:
throw new Error('Unsupported sub chunk version: ' + this.subChunkVersion)
}
for (let i = 0; i < storageCount; i++) {
const paletteType = stream.readByte()
const usingNetworkRuntimeIds = paletteType & 1
if (!usingNetworkRuntimeIds && (format === StorageType.Runtime)) {
throw new Error(`Expected network encoding while decoding SubChunk at y=${this.y}`)
}
const bitsPerBlock = paletteType >> 1
this.loadPalettedBlocks(i, stream, bitsPerBlock, format)
}
}
loadPalettedBlocks (storageLayer, stream, bitsPerBlock, format) {
const storage = new PalettedStorage(bitsPerBlock)
storage.read(stream)
this.blocks[storageLayer] = storage
const paletteSize = format === StorageType.LocalPersistence ? stream.readUInt32LE() : stream.readZigZagVarInt()
if (paletteSize > stream.buffer.length || paletteSize < 1) throw new Error(`Invalid palette size: ${paletteSize}`)
if (format === StorageType.Runtime) {
this.loadRuntimePalette(storageLayer, stream, paletteSize)
} else {
// Either "network persistent" (network with caching on <1.18) or local disk
this.loadLocalPalette(storageLayer, stream, paletteSize, format === StorageType.NetworkPersistence)
}
this.blocks[storageLayer].incrementPalette(this.palette[storageLayer])
}
loadRuntimePalette (storageLayer, stream, paletteSize) {
this.palette[storageLayer] = []
for (let i = 0; i < paletteSize; i++) {
const index = stream.readZigZagVarInt()
const block = this.registry.blockStates[index]
this.palette[storageLayer][i] = { stateId: index, ...block, count: 0 }
}
}
loadLocalPalette (storageLayer, stream, paletteSize, overNetwork) {
this.palette[storageLayer] = []
const buf = stream.buffer
let startOffset = stream.readOffset
let i
for (i = 0; i < paletteSize; i++) {
const { metadata, data } = nbt.protos[overNetwork ? 'littleVarint' : 'little'].parsePacketBuffer('nbt', buf, startOffset)
stream.readOffset += metadata.size // BinaryStream
startOffset += metadata.size // Buffer
const { name, states, version } = nbt.simplify(data)
let block = this.Block.fromProperties(name.replace('minecraft:', ''), states ?? {}, 0)
if (!block) {
// This is not a valid block
debugger // eslint-disable-line
block = this.Block.fromProperties('air', {}, 0)
}
this.palette[storageLayer][i] = { stateId: block.stateId, name, states: data.value.states.value, version, count: 0 }
}
if (i !== paletteSize) {
throw new Error(`Expected ${paletteSize} blocks, got ${i}`)
}
}
async encode (format, checksum = false, compact = true) {
const stream = new Stream()
if (this.subChunkVersion >= 8) {
this.encodeV8(stream, format, compact)
} else {
throw new Error('Unsupported sub chunk version')
}
const buf = stream.getBuffer()
if (checksum) {
this.hash = await getChecksum(buf)
this.updated = false
}
return buf
}
// Encode sub chunk version 8+
encodeV8 (stream, format, compact) {
stream.writeUInt8(this.subChunkVersion)
stream.writeUInt8(this.blocks.length)
if (this.subChunkVersion >= 9) { // Caves and cliffs (1.17-1.18)
stream.writeUInt8(this.y)
}
for (let l = 0; l < this.blocks.length; l++) {
if (compact) this.compact(l) // Compact before encoding
this.writeStorage(stream, l, format)
}
}
writeStorage (stream, storageLayer, format) {
const storage = this.blocks[storageLayer]
let paletteType = storage.bitsPerBlock << 1
if (format === StorageType.Runtime) {
paletteType |= 1
}
stream.writeUInt8(paletteType)
storage.write(stream)
if (format === StorageType.LocalPersistence) {
stream.writeUInt32LE(this.palette[storageLayer].length)
} else {
stream.writeZigZagVarInt(this.palette[storageLayer].length)
}
if (format === StorageType.Runtime) {
for (const block of this.palette[storageLayer]) {
stream.writeZigZagVarInt(block.stateId)
}
} else {
for (const block of this.palette[storageLayer]) {
const { name, states, version } = block
const tag = nbt.comp({ name: nbt.string(name), states: nbt.comp(states), version: nbt.int(version) })
const buf = nbt.writeUncompressed(tag, format === StorageType.LocalPersistence ? 'little' : 'littleVarint')
stream.writeBuffer(buf)
}
}
}
// Normal block access
getBlock (l, x, y, z, biomeId) {
if (l !== undefined) {
const stateId = this.getBlockStateId(l, x, y, z)
return this.Block.fromStateId(stateId, biomeId)
} else {
const layer1 = this.getBlockStateId(0, x, y, z)
const layer2 = this.getBlockStateId(1, x, y, z)
const block = this.Block.fromStateId(layer1, biomeId)
if (layer2) {
block.superimposed = this.Block.fromStateId(layer2, biomeId)
const name = block.superimposed.name
// TODO: Snowy blocks have to be handled in prismarine-viewer
if (name.includes('water')) {
block.computedStates.waterlogged = true
}
}
return block
}
}
setBlock (l, x, y, z, block) {
if (l !== undefined) {
this.setBlockStateId(l, x, y, z, block.stateId)
} else {
this.setBlockStateId(0, x, y, z, block.stateId)
if (block.superimposed) {
this.setBlockStateId(1, x, y, z, block.superimposed.stateId)
}
}
this.updated = true
}
getBlockStateId (l = 0, x, y, z) {
const blocks = this.blocks[l]
if (!blocks) {
return this.registry.blocksByName.air.defaultState
}
return this.palette[l][blocks.get(x, y, z)].stateId
}
setBlockStateId (l = 0, x, y, z, stateId) {
if (!this.palette[l]) {
this.palette[l] = []
this.blocks[l] = new PalettedStorage(4) // Zero initialized
this.addToPalette(l, this.registry.blocksByName.air.defaultState, 4096 - 1)
this.addToPalette(l, stateId, 1)
this.blocks[l].set(x, y, z, this.palette[l].length - 1)
} else {
// Decrement count of old block
const currentIndex = this.blocks[l].get(x, y, z)
const currentEntry = this.palette[l][currentIndex]
if (currentEntry.stateId === stateId) {
return // No change
}
currentEntry.count--
for (let i = 0; i < this.palette[l].length; i++) {
const entry = this.palette[l][i]
if (entry.stateId === stateId) {
entry.count = Math.max(entry.count, 0) + 1
this.blocks[l].set(x, y, z, i)
return
}
}
this.addToPalette(l, stateId, 1)
this.blocks[l].set(x, y, z, this.palette[l].length - 1)
}
this.updated = true
}
addToPalette (l, stateId, count = 0) {
const block = this.registry.blockStates[stateId]
this.palette[l].push({ stateId, name: block.name, states: block.states, count })
const minBits = neededBits(this.palette[l].length - 1)
if (minBits > this.blocks[l].bitsPerBlock) {
this.blocks[l] = this.blocks[l].resize(minBits)
}
}
// These compation functions reduces the size of the chunk by removing unused blocks
// and reordering the palette to reduce the number of bits per block
isCompactable (layer) {
let newPaletteLength = 0
for (const block of this.palette[layer]) {
if (block.count > 0) {
newPaletteLength++
}
}
return newPaletteLength < this.palette[layer].length
}
compact (layer) {
const newPalette = []
const map = []
for (const block of this.palette[layer]) {
if (block.count > 0) {
newPalette.push(block)
}
map.push(newPalette.length - 1)
}
if (newPalette.length === this.palette[layer].length) {
return
}
const newStorage = new PalettedStorage(neededBits(newPalette.length - 1))
for (let x = 0; x < 16; x++) {
for (let y = 0; y < 16; y++) {
for (let z = 0; z < 16; z++) {
const ix = this.blocks[layer].get(x, y, z)
newStorage.set(x, y, z, map[ix])
}
}
}
this.blocks[layer] = newStorage
this.palette[layer] = newPalette
}
/**
* Gets the block runtime ID at the layer and position
* @returns Global block palette (runtime) ID for the block
*/
getPaletteEntry (l, x, y, z) {
return this.palette[l][this.blocks[l].get(x, y, z)]
}
getPalette (layer = 0) {
return this.palette[layer].filter(block => block.count > 0)
}
// Lighting - Not written or read, but computed during chunk loading
getBlockLight (x, y, z) {
return this.blockLight.get(x, y, z)
}
setBlockLight (x, y, z, value) {
this.blockLight.set(x, y, z, value)
}
getSkyLight (x, y, z) {
return this.skyLight.get(x, y, z)
}
setSkyLight (x, y, z, value) {
this.skyLight.set(x, y, z, value)
}
toObject () {
return {
y: this.y,
palette: this.palette,
blocks: this.blocks,
subChunkVersion: this.subChunkVersion,
hash: this.hash,
updated: this.updated,
blockLight: this.blockLight,
skyLight: this.skyLight
}
}
}
module.exports = SubChunk