pages235/renderer/viewer/lib/worldrendererCommon.ts
2025-03-12 00:14:16 +03:00

686 lines
26 KiB
TypeScript

/* eslint-disable guard-for-in */
import { EventEmitter } from 'events'
import { Vec3 } from 'vec3'
import * as THREE from 'three'
import mcDataRaw from 'minecraft-data/data.js' // note: using alias
import blocksAtlases from 'mc-assets/dist/blocksAtlases.json'
import blocksAtlasLatest from 'mc-assets/dist/blocksAtlasLatest.png'
import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png'
import itemsAtlases from 'mc-assets/dist/itemsAtlases.json'
import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png'
import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
import { AtlasParser, getLoadedItemDefinitionsStore } from 'mc-assets'
import TypedEmitter from 'typed-emitter'
import { LineMaterial } from 'three-stdlib'
import christmasPack from 'mc-assets/dist/textureReplacements/christmas'
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
import itemDefinitionsJson from 'mc-assets/dist/itemDefinitions.json'
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
import { toMajorVersion } from '../../../src/utils'
import { buildCleanupDecorator } from './cleanupDecorator'
import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput, CustomBlockModels, BlockStateModelInfo } from './mesher/shared'
import { chunkPos } from './simpleUtils'
import { HandItemBlock } from './holdingBlock'
import { updateStatText } from './ui/newStats'
import { WorldRendererThree } from './worldrendererThree'
import { generateGuiAtlas } from './guiRenderer'
function mod (x, n) {
return ((x % n) + n) % n
}
export const worldCleanup = buildCleanupDecorator('resetWorld')
export const defaultWorldRendererConfig = {
showChunkBorders: false,
numWorkers: 4,
isPlayground: false,
renderEars: true,
// game renderer setting actually
showHand: false,
viewBobbing: false
}
export type WorldRendererConfig = typeof defaultWorldRendererConfig
type CustomTexturesData = {
tileSize: number | undefined
textures: Record<string, HTMLImageElement>
}
export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
// todo
@worldCleanup()
threejsCursorLineMaterial: LineMaterial
@worldCleanup()
cursorBlock = null as Vec3 | null
displayStats = true
@worldCleanup()
worldConfig = { minY: 0, worldHeight: 256 }
// todo need to cleanup
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
cameraRoll = 0
@worldCleanup()
active = false
version = undefined as string | undefined
// #region CHUNK & SECTIONS TRACKING
@worldCleanup()
loadedChunks = {} as Record<string, boolean> // data is added for these chunks and they might be still processing
@worldCleanup()
finishedChunks = {} as Record<string, boolean> // these chunks are fully loaded into the world (scene)
@worldCleanup()
finishedSections = {} as Record<string, boolean> // these sections are fully loaded into the world (scene)
@worldCleanup()
// loading sections (chunks)
sectionsWaiting = new Map<string, number>()
@worldCleanup()
queuedChunks = new Set<string>()
queuedFunctions = [] as Array<() => void>
// #endregion
@worldCleanup()
renderUpdateEmitter = new EventEmitter() as unknown as TypedEmitter<{
dirty (pos: Vec3, value: boolean): void
update (/* pos: Vec3, value: boolean */): void
textureDownloaded (): void
itemsTextureDownloaded (): void
chunkFinished (key: string): void
}>
customTexturesDataUrl = undefined as string | undefined
@worldCleanup()
currentTextureImage = undefined as any
workers: any[] = []
@worldCleanup()
viewerPosition?: Vec3
lastCamUpdate = 0
droppedFpsPercentage = 0
@worldCleanup()
initialChunkLoadWasStartedIn: number | undefined
@worldCleanup()
initialChunksLoad = true
enableChunksLoadDelay = false
texturesVersion?: string
viewDistance = -1
chunksLength = 0
@worldCleanup()
allChunksFinished = false
handleResize = () => { }
mesherConfig = defaultMesherConfig
camera: THREE.PerspectiveCamera
highestBlocks = new Map<string, HighestBlockInfo>()
blockstatesModels: any
customBlockStates: Record<string, any> | undefined
customModels: Record<string, any> | undefined
itemsAtlasParser: AtlasParser | undefined
blocksAtlasParser: AtlasParser | undefined
sourceData = {
blocksAtlases,
itemsAtlases,
itemDefinitionsJson
}
customTextures: {
items?: CustomTexturesData
blocks?: CustomTexturesData
armor?: CustomTexturesData
} = {}
itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceData.itemDefinitionsJson)
workersProcessAverageTime = 0
workersProcessAverageTimeCount = 0
maxWorkersProcessTime = 0
geometryReceiveCount = {}
allLoadedIn: undefined | number
rendererDevice = '...'
edgeChunks = {} as Record<string, boolean>
lastAddChunk = null as null | {
timeout: any
x: number
z: number
}
neighborChunkUpdates = true
lastChunkDistance = 0
debugStopGeometryUpdate = false
@worldCleanup()
freeFlyMode = false
@worldCleanup()
freeFlyState = {
yaw: 0,
pitch: 0,
position: new Vec3(0, 0, 0)
}
@worldCleanup()
itemsRenderer: ItemsRenderer | undefined
protocolCustomBlocks = new Map<string, CustomBlockModels>()
@worldCleanup()
blockStateModelInfo = new Map<string, BlockStateModelInfo>()
abstract outputFormat: 'threeJs' | 'webgpu'
worldBlockProvider: WorldBlockProvider
abstract changeBackgroundColor (color: [number, number, number]): void
constructor (public config: WorldRendererConfig) {
// this.initWorkers(1) // preload script on page load
this.snapshotInitialValues()
this.renderUpdateEmitter.on('update', () => {
const loadedChunks = Object.keys(this.finishedChunks).length
updateStatText('loaded-chunks', `${loadedChunks}/${this.chunksLength} chunks (${this.lastChunkDistance}/${this.viewDistance})`)
})
}
snapshotInitialValues () { }
initWorkers (numWorkers = this.config.numWorkers) {
// init workers
for (let i = 0; i < numWorkers + 1; i++) {
// Node environment needs an absolute path, but browser needs the url of the file
const workerName = 'mesher.js'
// eslint-disable-next-line node/no-path-concat
const src = typeof window === 'undefined' ? `${__dirname}/${workerName}` : workerName
let worker: any
if (process.env.SINGLE_FILE_BUILD) {
const workerCode = document.getElementById('mesher-worker-code')!.textContent!
const blob = new Blob([workerCode], { type: 'text/javascript' })
worker = new Worker(window.URL.createObjectURL(blob))
} else {
worker = new Worker(src)
}
const handleMessage = (data) => {
if (!this.active) return
if (data.type !== 'geometry' || !this.debugStopGeometryUpdate) {
this.handleWorkerMessage(data)
}
if (data.type === 'geometry') {
this.geometryReceiveCount[data.workerIndex] ??= 0
this.geometryReceiveCount[data.workerIndex]++
const geometry = data.geometry as MesherGeometryOutput
for (const [key, highest] of geometry.highestBlocks.entries()) {
const currHighest = this.highestBlocks.get(key)
if (!currHighest || currHighest.y < highest.y) {
this.highestBlocks.set(key, highest)
}
}
// for (const key in geometry.highestBlocks) {
// const highest = geometry.highestBlocks[key]
// if (!this.highestBlocks[key] || this.highestBlocks[key].y < highest.y) {
// this.highestBlocks[key] = highest
// }
// }
const chunkCoords = data.key.split(',').map(Number)
this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2])))
}
if (data.type === 'sectionFinished') { // on after load & unload section
if (!this.sectionsWaiting.get(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`)
this.sectionsWaiting.set(data.key, this.sectionsWaiting.get(data.key)! - 1)
if (this.sectionsWaiting.get(data.key) === 0) {
this.sectionsWaiting.delete(data.key)
this.finishedSections[data.key] = true
}
const chunkCoords = data.key.split(',').map(Number)
if (this.loadedChunks[`${chunkCoords[0]},${chunkCoords[2]}`]) { // ensure chunk data was added, not a neighbor chunk update
const loadingKeys = [...this.sectionsWaiting.keys()]
if (!loadingKeys.some(key => {
const [x, y, z] = key.split(',').map(Number)
return x === chunkCoords[0] && z === chunkCoords[2]
})) {
this.finishedChunks[`${chunkCoords[0]},${chunkCoords[2]}`] = true
this.renderUpdateEmitter.emit(`chunkFinished`, `${chunkCoords[0] / 16},${chunkCoords[2] / 16}`)
}
}
this.checkAllFinished()
this.renderUpdateEmitter.emit('update')
if (data.processTime) {
this.workersProcessAverageTimeCount++
this.workersProcessAverageTime = ((this.workersProcessAverageTime * (this.workersProcessAverageTimeCount - 1)) + data.processTime) / this.workersProcessAverageTimeCount
this.maxWorkersProcessTime = Math.max(this.maxWorkersProcessTime, data.processTime)
}
}
if (data.type === 'blockStateModelInfo') {
for (const [cacheKey, info] of Object.entries(data.info)) {
this.blockStateModelInfo.set(cacheKey, info)
}
}
}
worker.onmessage = ({ data }) => {
if (Array.isArray(data)) {
// eslint-disable-next-line unicorn/no-array-for-each
data.forEach(handleMessage)
return
}
handleMessage(data)
}
if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) })
this.workers.push(worker)
}
}
checkAllFinished () {
if (this.sectionsWaiting.size === 0) {
const allFinished = Object.keys(this.finishedChunks).length === this.chunksLength
if (allFinished) {
this.allChunksLoaded?.()
this.allChunksFinished = true
this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn!
}
}
}
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }
abstract handleWorkerMessage (data: WorkerReceive): void
abstract updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void
abstract render (): void
/**
* Optionally update data that are depedendent on the viewer position
*/
updatePosDataChunk? (key: string): void
allChunksLoaded? (): void
timeUpdated? (newTime: number): void
updateViewerPosition (pos: Vec3) {
this.viewerPosition = pos
for (const [key, value] of Object.entries(this.loadedChunks)) {
if (!value) continue
this.updatePosDataChunk?.(key)
}
}
sendWorkers (message: WorkerSend) {
for (const worker of this.workers) {
worker.postMessage(message)
}
}
getDistance (posAbsolute: Vec3) {
const [botX, botZ] = chunkPos(this.viewerPosition!)
const dx = Math.abs(botX - Math.floor(posAbsolute.x / 16))
const dz = Math.abs(botZ - Math.floor(posAbsolute.z / 16))
return [dx, dz] as [number, number]
}
abstract updateShowChunksBorder (value: boolean): void
resetWorld () {
// destroy workers
for (const worker of this.workers) {
worker.terminate()
}
this.workers = []
this.currentTextureImage = undefined
this.blocksAtlasParser = undefined
this.itemsAtlasParser = undefined
}
// new game load happens here
async setVersion (version, texturesVersion = version) {
if (!this.blockstatesModels) throw new Error('Blockstates models is not loaded yet')
this.version = version
this.texturesVersion = texturesVersion
this.resetWorld()
// for workers in single file build
if (document.readyState === 'loading') {
await new Promise(resolve => {
document.addEventListener('DOMContentLoaded', resolve)
})
}
this.initWorkers()
this.active = true
this.mesherConfig.outputFormat = this.outputFormat
this.mesherConfig.version = this.version!
this.sendMesherMcData()
await this.updateAssetsData()
}
sendMesherMcData () {
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajorVersion(this.version)]
const mcData = {
version: JSON.parse(JSON.stringify(allMcData.version))
}
for (const key of dynamicMcDataFiles) {
mcData[key] = allMcData[key]
}
for (const worker of this.workers) {
worker.postMessage({ type: 'mcData', mcData, config: this.mesherConfig })
}
}
async generateGuiTextures () {
await generateGuiAtlas()
}
async updateAssetsData (resourcePackUpdate = false, prioritizeBlockTextures?: string[]) {
const blocksAssetsParser = new AtlasParser(this.sourceData.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
const itemsAssetsParser = new AtlasParser(this.sourceData.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
const blockTexturesChanges = {} as Record<string, string>
const date = new Date()
if ((date.getMonth() === 11 && date.getDate() >= 24) || (date.getMonth() === 0 && date.getDate() <= 6)) {
Object.assign(blockTexturesChanges, christmasPack)
}
const customBlockTextures = Object.keys(this.customTextures.blocks?.textures ?? {})
const customItemTextures = Object.keys(this.customTextures.items?.textures ?? {})
console.time('createBlocksAtlas')
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
const texture = this.customTextures?.blocks?.textures[textureName]
return blockTexturesChanges[textureName] ?? texture
}, /* this.customTextures?.blocks?.tileSize */undefined, prioritizeBlockTextures, customBlockTextures)
console.timeEnd('createBlocksAtlas')
console.time('createItemsAtlas')
const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
const texture = this.customTextures?.items?.textures[textureName]
if (!texture) return
return texture
}, this.customTextures?.items?.tileSize, undefined, customItemTextures)
console.timeEnd('createItemsAtlas')
this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
this.itemsRenderer = new ItemsRenderer(this.version!, this.blockstatesModels, this.itemsAtlasParser, this.blocksAtlasParser)
this.worldBlockProvider = worldBlockProvider(this.blockstatesModels, this.blocksAtlasParser.atlas, 'latest')
const texture = await new THREE.TextureLoader().loadAsync(this.blocksAtlasParser.latestImage)
texture.magFilter = THREE.NearestFilter
texture.minFilter = THREE.NearestFilter
texture.flipY = false
this.material.map = texture
this.currentTextureImage = this.material.map.image
this.mesherConfig.textureSize = this.material.map.image.width
for (const [i, worker] of this.workers.entries()) {
const { blockstatesModels } = this
if (this.customBlockStates) {
// TODO! remove from other versions as well
blockstatesModels.blockstates.latest = {
...blockstatesModels.blockstates.latest,
...this.customBlockStates
}
}
if (this.customModels) {
blockstatesModels.models.latest = {
...blockstatesModels.models.latest,
...this.customModels
}
}
worker.postMessage({
type: 'mesherData',
workerIndex: i,
blocksAtlas: {
latest: blocksAtlas
},
blockstatesModels,
config: this.mesherConfig,
})
}
if (!this.itemsAtlasParser) return
const itemsTexture = await new THREE.TextureLoader().loadAsync(this.itemsAtlasParser.latestImage)
itemsTexture.magFilter = THREE.NearestFilter
itemsTexture.minFilter = THREE.NearestFilter
itemsTexture.flipY = false
viewer.entities.itemsTexture = itemsTexture
if (!this.itemsAtlasParser) return
this.renderUpdateEmitter.emit('textureDownloaded')
console.time('generateGuiTextures')
await this.generateGuiTextures()
console.timeEnd('generateGuiTextures')
if (!this.itemsAtlasParser) return
this.renderUpdateEmitter.emit('itemsTextureDownloaded')
console.log('textures loaded')
}
async downloadDebugAtlas (isItems = false) {
const atlasParser = (isItems ? this.itemsAtlasParser : this.blocksAtlasParser)!
const dataUrl = await atlasParser.createDebugImage(true)
const a = document.createElement('a')
a.href = dataUrl
a.download = `atlas-debug-${isItems ? 'items' : 'blocks'}.png`
a.click()
}
get worldMinYRender () {
return Math.floor(Math.max(this.worldConfig.minY, this.mesherConfig.clipWorldBelowY ?? -Infinity) / 16) * 16
}
updateChunksStatsText () {
updateStatText('downloaded-chunks', `${Object.keys(this.loadedChunks).length}/${this.chunksLength} chunks D (${this.workers.length}:${this.workersProcessAverageTime.toFixed(0)}ms/${this.allLoadedIn?.toFixed(1) ?? '-'}s)`)
}
addColumn (x: number, z: number, chunk: any, isLightUpdate: boolean) {
if (!this.active) return
if (this.workers.length === 0) throw new Error('workers not initialized yet')
this.initialChunksLoad = false
this.initialChunkLoadWasStartedIn ??= Date.now()
this.loadedChunks[`${x},${z}`] = true
this.updateChunksStatsText()
const chunkKey = `${x},${z}`
const customBlockModels = this.protocolCustomBlocks.get(chunkKey)
for (const worker of this.workers) {
worker.postMessage({
type: 'chunk',
x,
z,
chunk,
customBlockModels: customBlockModels || undefined
})
}
for (let y = this.worldMinYRender; y < this.worldConfig.worldHeight; y += 16) {
const loc = new Vec3(x, y, z)
this.setSectionDirty(loc)
if (this.neighborChunkUpdates && (!isLightUpdate || this.mesherConfig.smoothLighting)) {
this.setSectionDirty(loc.offset(-16, 0, 0))
this.setSectionDirty(loc.offset(16, 0, 0))
this.setSectionDirty(loc.offset(0, 0, -16))
this.setSectionDirty(loc.offset(0, 0, 16))
}
}
}
markAsLoaded (x, z) {
this.loadedChunks[`${x},${z}`] = true
this.finishedChunks[`${x},${z}`] = true
this.checkAllFinished()
}
removeColumn (x, z) {
delete this.loadedChunks[`${x},${z}`]
for (const worker of this.workers) {
worker.postMessage({ type: 'unloadChunk', x, z })
}
delete this.finishedChunks[`${x},${z}`]
this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength
if (!this.allChunksFinished) {
this.allLoadedIn = undefined
this.initialChunkLoadWasStartedIn = undefined
}
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
this.setSectionDirty(new Vec3(x, y, z), false)
delete this.finishedSections[`${x},${y},${z}`]
}
// remove from highestBlocks
const startX = Math.floor(x / 16) * 16
const startZ = Math.floor(z / 16) * 16
const endX = Math.ceil((x + 1) / 16) * 16
const endZ = Math.ceil((z + 1) / 16) * 16
for (let x = startX; x < endX; x += 16) {
for (let z = startZ; z < endZ; z += 16) {
delete this.highestBlocks[`${x},${z}`]
}
}
}
setBlockStateId (pos: Vec3, stateId: number) {
const needAoRecalculation = true
const chunkKey = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
const customBlockModels = this.protocolCustomBlocks.get(chunkKey) || {}
for (const worker of this.workers) {
worker.postMessage({
type: 'blockUpdate',
pos,
stateId,
customBlockModels
})
}
this.setSectionDirty(pos, true, true)
if (this.neighborChunkUpdates) {
if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0), true, true)
if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0), true, true)
if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0), true, true)
if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0), true, true)
if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16), true, true)
if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16), true, true)
if (needAoRecalculation) {
// top view neighbors
if ((pos.x & 15) === 0 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, -16), true, true)
if ((pos.x & 15) === 15 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(16, 0, -16), true, true)
if ((pos.x & 15) === 0 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(-16, 0, 16), true, true)
if ((pos.x & 15) === 15 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 16), true, true)
// side view neighbors (but ignore updates above)
// z view neighbors
if ((pos.x & 15) === 0 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(-16, -16, 0), true, true)
if ((pos.x & 15) === 15 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(16, -16, 0), true, true)
// x view neighbors
if ((pos.z & 15) === 0 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, -16), true, true)
if ((pos.z & 15) === 15 && (pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 16), true, true)
// x & z neighbors
if ((pos.y & 15) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(-16, -16, -16), true, true)
if ((pos.y & 15) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 0) this.setSectionDirty(pos.offset(16, -16, -16), true, true)
if ((pos.y & 15) === 0 && (pos.x & 15) === 0 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(-16, -16, 16), true, true)
if ((pos.y & 15) === 0 && (pos.x & 15) === 15 && (pos.z & 15) === 15) this.setSectionDirty(pos.offset(16, -16, 16), true, true)
}
}
}
queueAwaited = false
messagesQueue = {} as { [workerIndex: string]: any[] }
getWorkerNumber (pos: Vec3, updateAction = false) {
if (updateAction) {
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
const cantUseChangeWorker = this.sectionsWaiting.get(key) && !this.finishedSections[key]
if (!cantUseChangeWorker) return 0
}
const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length - 1)
return hash + 1
}
setSectionDirty (pos: Vec3, value = true, useChangeWorker = false) { // value false is used for unloading chunks
if (this.viewDistance === -1) throw new Error('viewDistance not set')
this.allChunksFinished = false
const distance = this.getDistance(pos)
if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
// if (this.sectionsOutstanding.has(key)) return
this.renderUpdateEmitter.emit('dirty', pos, value)
// Dispatch sections to workers based on position
// This guarantees uniformity accross workers and that a given section
// is always dispatched to the same worker
const hash = this.getWorkerNumber(pos, useChangeWorker)
this.sectionsWaiting.set(key, (this.sectionsWaiting.get(key) ?? 0) + 1)
this.messagesQueue[hash] ??= []
this.messagesQueue[hash].push({
// this.workers[hash].postMessage({
type: 'dirty',
x: pos.x,
y: pos.y,
z: pos.z,
value,
config: this.mesherConfig,
})
this.dispatchMessages()
}
dispatchMessages () {
if (this.queueAwaited) return
this.queueAwaited = true
setTimeout(() => {
// group messages and send as one
for (const workerIndex in this.messagesQueue) {
const worker = this.workers[Number(workerIndex)]
worker.postMessage(this.messagesQueue[workerIndex])
}
this.messagesQueue = {}
this.queueAwaited = false
})
}
// Listen for chunk rendering updates emitted if a worker finished a render and resolve if the number
// of sections not rendered are 0
async waitForChunksToRender () {
return new Promise<void>((resolve, reject) => {
if ([...this.sectionsWaiting].length === 0) {
resolve()
return
}
const updateHandler = () => {
if (this.sectionsWaiting.size === 0) {
this.renderUpdateEmitter.removeListener('update', updateHandler)
resolve()
}
}
this.renderUpdateEmitter.on('update', updateHandler)
})
}
async waitForChunkToLoad (pos: Vec3) {
return new Promise<void>((resolve, reject) => {
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`
if (this.loadedChunks[key]) {
resolve()
return
}
const updateHandler = () => {
if (this.loadedChunks[key]) {
this.renderUpdateEmitter.removeListener('update', updateHandler)
resolve()
}
}
this.renderUpdateEmitter.on('update', updateHandler)
})
}
destroy () {
console.warn('world destroy is not implemented')
}
abstract setHighlightCursorBlock (block: typeof this.cursorBlock, shapePositions?: Array<{ position; width; height; depth }>): void
}