prismarine-world
Version:
The core implementation of the world for prismarine
317 lines (275 loc) • 9.67 kB
JavaScript
const { Vec3 } = require('vec3')
const { EventEmitter } = require('events')
const { RaycastIterator } = require('./iterators')
const WorldSync = require('./worldsync')
const { once } = require('events')
function columnKeyXZ (chunkX, chunkZ) {
return chunkX + ',' + chunkZ
}
function posInChunk (pos) {
return new Vec3(Math.floor(pos.x) & 15, Math.floor(pos.y), Math.floor(pos.z) & 15)
}
class World extends EventEmitter {
constructor (chunkGenerator, storageProvider = null, savingInterval = 1000) {
super()
this.savingQueue = new Map()
this.unloadQueue = new Map()
this.finishedSaving = Promise.resolve()
this.currentlySaving = false // semaphore for saving
this.columns = {}
this.chunkGenerator = chunkGenerator
this.storageProvider = storageProvider
this.savingInterval = savingInterval
this.sync = new WorldSync(this)
if (storageProvider && savingInterval !== 0) this.startSaving()
}
initialize (iniFunc, length, width, height = 256, iniPos = new Vec3(0, 0, 0)) {
function inZone (x, y, z) {
if (x >= width || x < 0) { return false }
if (z >= length || z < 0) {
return false
}
if (y >= height || y < 0) { return false }
return true
}
const ps = []
const iniPosInChunk = posInChunk(iniPos)
const chunkLength = Math.ceil((length + iniPosInChunk.z) / 16)
const chunkWidth = Math.ceil((width + iniPosInChunk.x) / 16)
for (let chunkZ = 0; chunkZ < chunkLength; chunkZ++) {
const actualChunkZ = chunkZ + Math.floor(iniPos.z / 16)
for (let chunkX = 0; chunkX < chunkWidth; chunkX++) {
const actualChunkX = chunkX + Math.floor(iniPos.x / 16)
ps.push(this.getColumn(actualChunkX, actualChunkZ)
.then(chunk => {
const offsetX = chunkX * 16 - iniPosInChunk.x
const offsetZ = chunkZ * 16 - iniPosInChunk.z
chunk.initialize((x, y, z) => inZone(x + offsetX, y - iniPos.y, z + offsetZ) ? iniFunc(x + offsetX, y - iniPos.y, z + offsetZ) : null)
return this.setColumn(actualChunkX, actualChunkZ, chunk)
})
.then(() => ({ chunkX: actualChunkX, chunkZ: actualChunkZ })))
}
}
return Promise.all(ps)
}
async raycast (from, direction, range, matcher = null) {
const iter = new RaycastIterator(from, direction, range)
let pos = iter.next()
while (pos) {
const position = new Vec3(pos.x, pos.y, pos.z)
const block = await this.getBlock(position)
if (block && (!matcher || matcher(block))) {
const intersect = iter.intersect(block.shapes, position)
if (intersect) {
block.face = intersect.face
block.intersect = intersect.pos
return block
}
}
pos = iter.next()
}
return null
}
getLoadedColumn (chunkX, chunkZ) {
const key = columnKeyXZ(chunkX, chunkZ)
return this.columns[key]
}
async getColumn (chunkX, chunkZ) {
await Promise.resolve()
const key = columnKeyXZ(chunkX, chunkZ)
if (!this.columns[key]) {
let chunk = null
if (this.storageProvider != null) {
const data = await this.storageProvider.load(chunkX, chunkZ)
if (data != null) { chunk = data }
}
const loaded = chunk != null
if (!loaded && this.chunkGenerator) {
chunk = this.chunkGenerator(chunkX, chunkZ)
}
if (chunk != null) { await this.setColumn(chunkX, chunkZ, chunk, !loaded) }
}
return this.columns[key]
}
_emitBlockUpdate (oldBlock, newBlock, position) {
oldBlock.position = position.floored()
newBlock.position = oldBlock.position
this.emit('blockUpdate', oldBlock, newBlock)
this.emit(`blockUpdate:${position}`, oldBlock, newBlock)
}
setLoadedColumn (chunkX, chunkZ, chunk, save = true) {
const key = columnKeyXZ(chunkX, chunkZ)
this.columns[key] = chunk
const columnCorner = new Vec3(chunkX * 16, 0, chunkZ * 16)
this.emit('chunkColumnLoad', columnCorner)
if (this.storageProvider && save) { this.queueSaving(chunkX, chunkZ) }
}
async setColumn (chunkX, chunkZ, chunk, save = true) {
await Promise.resolve()
this.setLoadedColumn(chunkX, chunkZ, chunk, save)
}
unloadColumn (chunkX, chunkZ) {
const key = columnKeyXZ(chunkX, chunkZ)
if (this.storageProvider && this.savingQueue.has(key)) {
this.unloadQueue.set(key, { chunkX, chunkZ })
} else {
this.forceUnloadColumn(key, chunkX, chunkZ)
}
}
forceUnloadColumn (key, chunkX, chunkZ) {
if (this.unloadQueue.has(key)) {
this.unloadQueue.delete(key)
}
delete this.columns[key]
const columnCorner = new Vec3(chunkX * 16, 0, chunkZ * 16)
this.emit('chunkColumnUnload', columnCorner)
}
async saveNow () {
if (this.savingQueue.size === 0) {
return
}
// We could set a limit on the number of chunks to save at each
// interval. The set structure is maintaining the order of insertion
for (const [key, { chunkX, chunkZ }] of this.savingQueue.entries()) {
this.finishedSaving = Promise.all([this.finishedSaving,
this.storageProvider.save(chunkX, chunkZ, this.columns[key])
])
}
await this.finishedSaving
this.savingQueue.clear()
for (const [key, { chunkX, chunkZ }] of this.unloadQueue.entries()) {
this.forceUnloadColumn(key, chunkX, chunkZ)
}
this.emit('doneSaving')
}
startSaving () {
this.savingInt = setInterval(async () => {
if (this.currentlySaving === false) {
this.currentlySaving = true
await this.saveNow()
this.currentlySaving = false
}
}, this.savingInterval)
}
async waitSaving () {
await this.saveNow()
if (this.savingQueue.size > 0) {
await once(this, 'doneSaving')
}
await this.finishedSaving
}
stopSaving () {
clearInterval(this.savingInt)
}
queueSaving (chunkX, chunkZ) {
this.savingQueue.set(columnKeyXZ(chunkX, chunkZ), { chunkX, chunkZ })
}
saveAt (pos) {
const chunkX = Math.floor(pos.x / 16)
const chunkZ = Math.floor(pos.z / 16)
if (this.storageProvider) { this.queueSaving(chunkX, chunkZ) }
}
getColumns () {
return Object.entries(this.columns).map(([key, column]) => {
const parts = key.split(',')
return {
chunkX: parts[0],
chunkZ: parts[1],
column
}
})
}
getLoadedColumnAt (pos) {
const chunkX = Math.floor(pos.x / 16)
const chunkZ = Math.floor(pos.z / 16)
return this.getLoadedColumn(chunkX, chunkZ)
}
async getColumnAt (pos) {
const chunkX = Math.floor(pos.x / 16)
const chunkZ = Math.floor(pos.z / 16)
return this.getColumn(chunkX, chunkZ)
}
async setBlock (pos, block) {
const chunk = (await this.getColumnAt(pos))
const pInChunk = posInChunk(pos)
const oldBlock = chunk.getBlock(pInChunk)
chunk.setBlock(pInChunk, block)
this.saveAt(pos)
this._emitBlockUpdate(oldBlock, block, pos)
}
async getBlock (pos) {
const block = (await this.getColumnAt(pos)).getBlock(posInChunk(pos))
block.position = pos.floored()
return block
}
async getBlockStateId (pos) {
return (await this.getColumnAt(pos)).getBlockStateId(posInChunk(pos))
}
async getBlockType (pos) {
return (await this.getColumnAt(pos)).getBlockType(posInChunk(pos))
}
async getBlockData (pos) {
return (await this.getColumnAt(pos)).getBlockData(posInChunk(pos))
}
async getBlockLight (pos) {
return (await this.getColumnAt(pos)).getBlockLight(posInChunk(pos))
}
async getSkyLight (pos) {
return (await this.getColumnAt(pos)).getSkyLight(posInChunk(pos))
}
async getBiome (pos) {
return (await this.getColumnAt(pos)).getBiome(posInChunk(pos))
}
async setBlockStateId (pos, stateId) {
const chunk = (await this.getColumnAt(pos))
const pInChunk = posInChunk(pos)
const oldBlock = chunk.getBlock(pInChunk)
chunk.setBlockStateId(pInChunk, stateId)
this.saveAt(pos)
this._emitBlockUpdate(oldBlock, chunk.getBlock(pInChunk), pos)
}
async setBlockType (pos, blockType) {
const chunk = (await this.getColumnAt(pos))
const pInChunk = posInChunk(pos)
const oldBlock = chunk.getBlock(pInChunk)
chunk.setBlockType(pInChunk, blockType)
this.saveAt(pos)
this._emitBlockUpdate(oldBlock, chunk.getBlock(pInChunk), pos)
}
async setBlockData (pos, data) {
const chunk = (await this.getColumnAt(pos))
const pInChunk = posInChunk(pos)
const oldBlock = chunk.getBlock(pInChunk)
chunk.setBlockData(pInChunk, data)
this.saveAt(pos)
this._emitBlockUpdate(oldBlock, chunk.getBlock(pInChunk), pos)
}
async setBlockLight (pos, light) {
const chunk = (await this.getColumnAt(pos))
const pInChunk = posInChunk(pos)
const oldBlock = chunk.getBlock(pInChunk)
chunk.setBlockLight(pInChunk, light)
this.saveAt(pos)
this._emitBlockUpdate(oldBlock, chunk.getBlock(pInChunk), pos)
}
async setSkyLight (pos, light) {
const chunk = (await this.getColumnAt(pos))
const pInChunk = posInChunk(pos)
const oldBlock = chunk.getBlock(pInChunk)
chunk.setSkyLight(pInChunk, light)
this.saveAt(pos)
this._emitBlockUpdate(oldBlock, chunk.getBlock(pInChunk), pos)
}
async setBiome (pos, biome) {
const chunk = (await this.getColumnAt(pos))
const pInChunk = posInChunk(pos)
const oldBlock = chunk.getBlock(pInChunk)
chunk.setBiome(pInChunk, biome)
this.saveAt(pos)
this._emitBlockUpdate(oldBlock, chunk.getBlock(pInChunk), pos)
}
}
function loader (mcVersion) {
return World
}
module.exports = loader