import {randomUUID} from 'node:crypto'; import {setImmediate as yield_turn} from 'node:timers/promises'; import type {WorldChunk, WorldPayloads, WorldSnapshot, WorldVoxelChunk} from '../../types/forward.ts'; import {ForwardFailure} from '../../utils/forward_protocol.ts'; type Position = {x: number; y: number; z: number}; export interface WorldColumn {getBlockStateId(position: Position): number} type LoadedWorld = {getColumn(x: number, z: number): WorldColumn | null | undefined}; export interface WorldBot { world: Partial & {sync?: LoadedWorld}; game: {minY: number; height: number; dimension: string}; entity: {position: Position; yaw: number}; registry: {blocksByStateId: Record}>; blocksByName?: Record}; version: string; on(event: any, listener: (...args: any[]) => void): unknown; off(event: any, listener: (...args: any[]) => void): unknown; } type Watch = {radius: number; expires_at: number; world_generation?: string}; type ColumnCache = {column: WorldColumn; revision: string; chunk?: WorldChunk}; const air = new Set(['air', 'cave_air', 'void_air']); const water_blocks = new Set(['water', 'bubble_column', 'kelp', 'kelp_plant', 'seagrass', 'tall_seagrass']); const colors: Record = { air: 0, grass_block: 0x78a54a, short_grass: 0x76a644, tall_grass: 0x76a644, fern: 0x548c3b, large_fern: 0x548c3b, moss_block: 0x637e36, dirt: 0x866043, coarse_dirt: 0x79553a, podzol: 0x674b2a, mud: 0x49403b, mycelium: 0x766376, water: 0x3f76e4, bubble_column: 0x3f76e4, lava: 0xf36b19, stone: 0x858585, cobblestone: 0x787878, deepslate: 0x505054, bedrock: 0x555555, granite: 0x956856, diorite: 0xbcbcbc, andesite: 0x888a88, tuff: 0x6c716a, sand: 0xdbd29d, red_sand: 0xb96621, sandstone: 0xd5c58b, gravel: 0x8a817d, clay: 0x9ba4b1, snow: 0xf3fafa, snow_block: 0xf3fafa, ice: 0x91b8ef, packed_ice: 0x8db0e7, blue_ice: 0x6da3ed, glass: 0xb4d6d9, netherrack: 0x713232, soul_sand: 0x514035, soul_soil: 0x4b392f, end_stone: 0xd9dda1, obsidian: 0x30243f, bricks: 0x965f51, terracotta: 0x9a5e44, oak_leaves: 0x4e7930, birch_leaves: 0x76a64c, spruce_leaves: 0x3e633c, jungle_leaves: 0x4d822c, cherry_leaves: 0xe5a6bf, }; function block_color(name: string): number { if (Object.hasOwn(colors, name)) return colors[name]!; if (name.endsWith('_leaves')) return 0x50843c; if (name.endsWith('_log') || name.endsWith('_wood')) return 0x715334; if (name.endsWith('_planks')) return 0xb38b56; if (name.includes('stone') || name.endsWith('_ore')) return 0x858585; let hash = 2166136261; for (let index = 0; index < name.length; index++) hash = Math.imul(hash ^ name.charCodeAt(index), 16777619); return (64 + ((hash >>> 16) & 127)) << 16 | (64 + ((hash >>> 8) & 127)) << 8 | (64 + (hash & 127)); } function fluid_level(state: number, block: WorldBot['registry']['blocksByStateId'][number]): number { if (!Number.isSafeInteger(block.minStateId) || state < block.minStateId! || !block.states) return 0; let offset = state - block.minStateId!; // Registry state IDs are mixed-radix indices; the last property varies fastest. for (let index = block.states.length - 1; index >= 0; index--) { const property = block.states[index]!; if (!Number.isSafeInteger(property.num_values) || property.num_values < 1) return 0; const value_index = offset % property.num_values; offset = Math.floor(offset / property.num_values); if (property.name === 'level') { const level = Number(property.values?.[value_index] ?? value_index); return Number.isInteger(level) && level >= 0 && level <= 15 ? level : 0; } } return 0; } type SpikeSegment = NonNullable['segments'][number]; function spike_properties(state: number, block: WorldBot['registry']['blocksByStateId'][number]) { const result: Pick = {direction: 'up', thickness: 'tip'}; if (!Number.isSafeInteger(block.minStateId) || state < block.minStateId! || !block.states) return result; let offset = state - block.minStateId!; for (let index = block.states.length - 1; index >= 0; index--) { const property = block.states[index]!; if (!Number.isSafeInteger(property.num_values) || property.num_values < 1) return {direction: 'up', thickness: 'tip'} as const; const value = property.values?.[offset % property.num_values]; offset = Math.floor(offset / property.num_values); if (property.name === 'vertical_direction' && (value === 'up' || value === 'down')) result.direction = value; if (property.name === 'thickness' && (value === 'tip' || value === 'tip_merge' || value === 'frustum' || value === 'middle' || value === 'base')) result.thickness = value; } return result; } const feature_properties = new Set(['facing', 'rotation', 'half', 'type', 'shape', 'open', 'attached', 'lit', 'waterlogged', 'east', 'west', 'north', 'south', 'axis', 'up', 'vertical_direction', 'thickness']); function surface_properties(state: number, block: WorldBot['registry']['blocksByStateId'][number]): Record { const result: Record = {}; if (!Number.isSafeInteger(block.minStateId) || state < block.minStateId! || !block.states) return result; let offset = state - block.minStateId!; for (let i = block.states.length - 1; i >= 0; i--) { const property = block.states[i]!; if (!Number.isSafeInteger(property.num_values) || property.num_values < 1) return {}; const index = offset % property.num_values; offset = Math.floor(offset / property.num_values); if (!feature_properties.has(property.name)) continue; const value = property.values?.[index] ?? (property.num_values === 2 ? index === 0 : index); if (property.name === 'rotation') result.rotation = Number(value); else if (value === 'true' || value === 'false') result[property.name] = value === 'true'; else if (typeof value === 'string' || typeof value === 'boolean') result[property.name] = value; } return result; } function surface_feature(name: string, properties: Record): boolean { return properties.waterlogged === true || /(?:^|_)(?:wall_)?torch$|_(?:sign|slab|stairs|fence|pane|trapdoor|wall)$/.test(name); } function voxel_properties(state: number, block: WorldBot['registry']['blocksByStateId'][number]): Record { const result: Record = {}; if (!block.states?.length) return result; if (block.states.length > 32 || !Number.isSafeInteger(block.minStateId) || state < block.minStateId!) throw new ForwardFailure('INTERNAL_ERROR'); let offset = state - block.minStateId!; for (let i = block.states.length - 1; i >= 0; i--) { const property = block.states[i]!; if (typeof property.name !== 'string' || !property.name.length || property.name.length > 32 || ['__proto__', 'prototype', 'constructor'].includes(property.name) || Object.hasOwn(result, property.name) || !Number.isSafeInteger(property.num_values) || property.num_values < 1) throw new ForwardFailure('INTERNAL_ERROR'); const index = offset % property.num_values; offset = Math.floor(offset / property.num_values); const value = property.type === 'bool' ? index === 0 : property.type === 'int' ? Number(property.values?.[index] ?? index) : property.values?.[index] ?? index; if (typeof value === 'string' ? value.length > 64 : typeof value === 'number' ? !Number.isFinite(value) : typeof value !== 'boolean') throw new ForwardFailure('INTERNAL_ERROR'); result[property.name] = value as string | number | boolean; } if (offset) throw new ForwardFailure('INTERNAL_ERROR'); return result; } function voxel_occluding(name: string, block: WorldBot['registry']['blocksByStateId'][number]): boolean { // Registry boundingBox describes collision presence, not whether it fills the cube. return block.boundingBox === 'block' && block.transparent !== true && block.filterLight !== 0 && !air.has(name) && !/(?:^|_)(?:slab|stairs|fence|gate|wall|pane|door|trapdoor|torch|leaves|glass|water|lava|snow|cake|candle|carpet|bed|button|pressure_plate|sign|banner|head|skull|coral|fan|flower|sapling|plant|roots|rail)$/.test(name) && !['farmland', 'dirt_path', 'soul_sand', 'enchanting_table', 'brewing_stand', 'cauldron', 'hopper', 'anvil', 'chipped_anvil', 'damaged_anvil', 'cactus', 'dragon_egg', 'end_portal_frame', 'piston_head', 'moving_piston', 'chest', 'trapped_chest', 'ender_chest', 'bell', 'stonecutter', 'grindstone', 'lectern', 'composter', 'daylight_detector', 'sculk_sensor', 'calibrated_sculk_sensor'].includes(name); } export class WorldPreview { private generation = randomUUID(); private identity?: {world: WorldBot['world']; version: string; dimension: string; minY: number; height: number}; private readonly watches = new Map(); private readonly cache = new Map(); private readonly voxels = new Map(); private listeners: Array<[string, (...args: any[]) => void]> = []; private expiry?: ReturnType; private invalidation?: ReturnType; private center?: {x: number; z: number}; private disposed = false; private pending = 0; private pending_voxels = 0; constructor(readonly bot: WorldBot, private readonly hint: (generation: string) => void, private readonly now: () => number = Date.now) {} private metadata() { const {game, world, version, registry, entity} = this.bot; // Mineflayer exposes WorldSync directly; adapters may expose the parent World's .sync. const sync = world?.sync ?? world; if (!sync || typeof sync.getColumn !== 'function' || !registry?.blocksByStateId || !game || !Number.isSafeInteger(game.minY) || game.minY < -4096 || game.minY > 4096 || !Number.isSafeInteger(game.height) || game.height < 1 || game.height > 4096 || typeof game.dimension !== 'string' || !game.dimension.length || game.dimension.length > 256 || typeof version !== 'string' || !version.length || version.length > 64 || !entity?.position || ![entity.position.x, entity.position.y, entity.position.z, entity.yaw].every(Number.isFinite)) { throw new ForwardFailure('INTERNAL_ERROR'); } const center = {x: Math.floor(entity.position.x / 16), z: Math.floor(entity.position.z / 16)}; if (!Number.isSafeInteger(center.x) || !Number.isSafeInteger(center.z) || Math.abs(center.x) > Number.MAX_SAFE_INTEGER - 3 || Math.abs(center.z) > Number.MAX_SAFE_INTEGER - 3) throw new ForwardFailure('INTERNAL_ERROR'); return {world, sync: sync as LoadedWorld, version, dimension: game.dimension, minY: game.minY, height: game.height, center, position: {x: entity.position.x, y: entity.position.y, z: entity.position.z, yaw: entity.yaw}}; } private refresh() { const metadata = this.metadata(); const previous = this.identity; if (!previous || previous.world !== metadata.world || previous.version !== metadata.version || previous.dimension !== metadata.dimension || previous.minY !== metadata.minY || previous.height !== metadata.height) { this.generation = randomUUID(); this.cache.clear(); this.voxels.clear(); this.identity = metadata; if (previous) this.invalidate(); } if (this.center && (this.center.x !== metadata.center.x || this.center.z !== metadata.center.z)) this.invalidate(); this.center = metadata.center; return metadata; } private invalidate() { if (this.disposed || !this.watches.size || this.invalidation) return; this.invalidation = setTimeout(() => { this.invalidation = undefined; this.prune(); if (!this.disposed && this.watches.size) this.hint(this.generation); }, 200); this.invalidation.unref(); } private change(position?: {x: number; z: number}) { this.prune(); if (!this.watches.size || this.disposed) return; const chunk = position && Number.isFinite(position.x) && Number.isFinite(position.z) ? {x: Math.floor(position.x / 16), z: Math.floor(position.z / 16)} : undefined; // Invalidate immediately even if position/game metadata is transient during respawn. if (chunk) this.forget(`${chunk.x},${chunk.z}`); try { const metadata = this.refresh(); if (chunk && [...this.watches.values()].some(watch => Math.abs(chunk.x - metadata.center.x) <= watch.radius && Math.abs(chunk.z - metadata.center.z) <= watch.radius)) this.invalidate(); } catch { /* Metadata can be transiently unavailable during respawn; the next snapshot rechecks it. */ } } private attach() { if (this.listeners.length) return; const listen = (name: string, callback: (...args: any[]) => void) => { this.listeners.push([name, callback]); this.bot.on(name, callback); }; listen('chunkColumnLoad', position => this.change(position)); listen('chunkColumnUnload', position => this.change(position)); listen('blockUpdate', (old_block, new_block) => this.change(new_block?.position ?? old_block?.position)); listen('move', () => this.change()); listen('respawn', () => { if (!this.watches.size || this.disposed) return; // Respawning in the same dimension can retain the same world object. this.identity = undefined; try { this.refresh(); } catch { this.generation = randomUUID(); this.cache.clear(); this.voxels.clear(); } this.invalidate(); }); listen('end', () => this.dispose()); } private idle() { for (const [name, listener] of this.listeners) this.bot.off(name, listener); this.listeners = []; clearTimeout(this.expiry); clearTimeout(this.invalidation); this.expiry = this.invalidation = undefined; this.cache.clear(); this.voxels.clear(); this.identity = undefined; this.center = undefined; } private prune() { for (const [id, watch] of this.watches) if (watch.expires_at <= this.now()) this.watches.delete(id); if (!this.watches.size) this.idle(); } private arm_expiry() { clearTimeout(this.expiry); if (!this.watches.size) return; const expires = Math.min(...[...this.watches.values()].map(watch => watch.expires_at)); this.expiry = setTimeout(() => { this.prune(); this.arm_expiry(); }, Math.max(1, expires - this.now())); this.expiry.unref(); } unwatch(id: string): {released: true} { this.watches.delete(id); this.prune(); this.arm_expiry(); return {released: true}; } get is_disposed() { return this.disposed; } private forget(key: string) { this.cache.delete(key); this.voxels.delete(key); } private column_entry(key: string, column: WorldColumn): ColumnCache { let entry = this.cache.get(key); if (!entry || entry.column !== column) { entry = {column, revision: randomUUID()}; this.forget(key); this.cache.set(key, entry); while (this.cache.size > 96) this.forget(this.cache.keys().next().value!); } return entry; } async snapshot(input: WorldPayloads['world.snapshot'], check: () => void): Promise> { check(); if (this.disposed) throw new ForwardFailure('CANCELLED'); this.prune(); if (this.pending >= 4) throw new ForwardFailure('RATE_LIMITED'); const existing = input.watch_id && this.watches.has(input.watch_id) ? input.watch_id : undefined; if (!existing && this.watches.size >= 64) throw new ForwardFailure('RATE_LIMITED'); const watch_id = existing ?? randomUUID(); const watch: Watch = {radius: input.radius, expires_at: this.now() + 45_000}; this.watches.set(watch_id, watch); this.attach(); this.arm_expiry(); this.pending++; try { const metadata = this.refresh(); const generation = this.generation; watch.world_generation = generation; const valid = () => { check(); if (this.disposed || this.watches.get(watch_id) !== watch || watch.expires_at <= this.now()) throw new ForwardFailure('CANCELLED'); const current = this.refresh(); if (generation !== this.generation) throw new ForwardFailure('CANCELLED'); if (current.center.x !== metadata.center.x || current.center.z !== metadata.center.z) throw new ForwardFailure('CANCELLED'); }; const known = new Map(input.world_generation === generation ? input.known.map(item => [`${item.x},${item.z}`, item.revision]) : []); const chunks: WorldChunk[] = [], loaded: WorldSnapshot['loaded'] = []; const sampled: Array<{x: number; z: number; entry: ColumnCache}> = []; let partial = false; for (let z = metadata.center.z - input.radius; z <= metadata.center.z + input.radius; z++) { for (let x = metadata.center.x - input.radius; x <= metadata.center.x + input.radius; x++) { valid(); const key = `${x},${z}`; const column = metadata.sync.getColumn(x, z); if (!column) { this.forget(key); partial = true; continue; } const entry = this.column_entry(key, column); if (known.get(key) !== entry.revision) { if (!entry.chunk) entry.chunk = await this.scan(x, z, entry, metadata, () => { valid(); if (this.cache.get(key) !== entry || metadata.sync.getColumn(x, z) !== column) throw new ForwardFailure('CANCELLED'); }); valid(); if (this.cache.get(key) !== entry || metadata.sync.getColumn(x, z) !== column) { throw new ForwardFailure('CANCELLED'); } chunks.push(entry.chunk); } sampled.push({x, z, entry}); loaded.push({x, z, revision: entry.revision}); if (loaded.length % 2 === 0) { await yield_turn(); valid(); } } } valid(); // A column may change during a later column's yield. Never label stale data as current. for (const {x, z, entry} of sampled) { if (this.cache.get(`${x},${z}`) === entry && metadata.sync.getColumn(x, z) === entry.column) continue; throw new ForwardFailure('CANCELLED'); } return {world_generation: generation, watch_id, version: metadata.version, dimension: metadata.dimension, min_y: metadata.minY, height: metadata.height, center: metadata.center, position: metadata.position, chunks: structuredClone(chunks), loaded, partial}; } catch (error) { if (!existing && this.watches.get(watch_id) === watch) this.unwatch(watch_id); throw error; } finally { this.pending--; } } async chunk(input: WorldPayloads['world.chunk'], check: () => void): Promise { check(); if (this.disposed) throw new ForwardFailure('CANCELLED'); this.prune(); const watch = this.watches.get(input.watch_id); if (!watch) throw new ForwardFailure('CANCELLED'); const metadata = this.refresh(), generation = this.generation; if (input.world_generation !== generation || watch.world_generation !== generation) throw new ForwardFailure('CANCELLED'); if (Math.abs(input.x - metadata.center.x) > watch.radius || Math.abs(input.z - metadata.center.z) > watch.radius) throw new ForwardFailure('FORBIDDEN'); if (this.pending_voxels >= 2) throw new ForwardFailure('RATE_LIMITED'); const key = `${input.x},${input.z}`, column = metadata.sync.getColumn(input.x, input.z); if (!column) { this.forget(key); throw new ForwardFailure('CANCELLED'); } const entry = this.column_entry(key, column); const valid = () => { check(); if (this.disposed || this.watches.get(input.watch_id) !== watch || watch.expires_at <= this.now()) throw new ForwardFailure('CANCELLED'); const current = this.refresh(); if (generation !== this.generation || this.cache.get(key) !== entry || current.center.x !== metadata.center.x || current.center.z !== metadata.center.z || current.sync.getColumn(input.x, input.z) !== column) throw new ForwardFailure('CANCELLED'); }; this.pending_voxels++; try { valid(); const cached = this.voxels.get(key); if (cached?.entry === entry) { this.voxels.delete(key); this.voxels.set(key, cached); return structuredClone(cached.chunk); } const palette: WorldVoxelChunk['palette'] = [], sections: WorldVoxelChunk['sections'] = []; const indices = new Map(); const palette_index = (state: number) => { let index = indices.get(state); if (index !== undefined) return index; const block = this.bot.registry.blocksByStateId[state]; if (!Number.isSafeInteger(state) || state < 0 || !block || typeof block.name !== 'string' || !block.name.length || block.name.length > 128 || block.maxStateId !== undefined && state > block.maxStateId || palette.length >= 8192) throw new ForwardFailure('INTERNAL_ERROR'); const name = block.name.replace(/^minecraft:/, ''); index = palette.length; indices.set(state, index); palette.push({name: block.name, color: block_color(name), properties: voxel_properties(state, block), occluding: voxel_occluding(name, block)}); return index; }; const position = {x: 0, y: 0, z: 0}, reader = column.getBlockStateId; const read = reader.length >= 3 ? () => (reader as unknown as (x: number, y: number, z: number) => number).call(column, position.x, position.y, position.z) : () => reader.call(column, position); let reads = 0; for (let section = 0; section < Math.ceil(metadata.height / 16); section++) { const blocks = Buffer.alloc(8192); let non_air = false; for (let y = 0; y < 16; y++) { const offset_y = section * 16 + y; if (offset_y >= metadata.height) { // Padding is outside the world and must not duplicate the topmost solid block. if (non_air) { const registry = this.bot.registry; const air_state = registry.blocksByName?.air?.defaultState ?? registry.blocksByName?.air?.minStateId ?? Object.entries(registry.blocksByStateId).find(([, block]) => block.name.replace(/^minecraft:/, '') === 'air')?.[0]; if (air_state === undefined) throw new ForwardFailure('INTERNAL_ERROR'); const index = palette_index(Number(air_state)); for (let cell = y * 256; cell < 4096; cell++) blocks.writeUInt16LE(index, cell * 2); } break; } position.y = metadata.minY + offset_y; for (let z = 0; z < 16; z++) for (let x = 0; x < 16; x++) { position.x = x; position.z = z; const index = palette_index(read()); blocks.writeUInt16LE(index, (y * 256 + z * 16 + x) * 2); if (!air.has(palette[index]!.name.replace(/^minecraft:/, ''))) non_air = true; if (++reads % 2048 === 0) { await yield_turn(); valid(); } } } if (non_air) sections.push({y: section, blocks: blocks.toString('base64')}); } valid(); const chunk: WorldVoxelChunk = {x: input.x, z: input.z, revision: entry.revision, world_generation: generation, min_y: metadata.minY, height: metadata.height, palette, sections}; this.voxels.delete(key); this.voxels.set(key, {entry, chunk, bytes: Buffer.byteLength(JSON.stringify(chunk))}); let bytes = [...this.voxels.values()].reduce((sum, value) => sum + value.bytes, 0); while (this.voxels.size > 8 || bytes > 32 * 1024 * 1024) { const oldest = this.voxels.keys().next().value!; bytes -= this.voxels.get(oldest)!.bytes; this.voxels.delete(oldest); } return structuredClone(chunk); } finally { this.pending_voxels--; } } private async scan(x: number, z: number, entry: ColumnCache, metadata: ReturnType, check: () => void): Promise { const heights = Buffer.alloc(512), blocks = Buffer.alloc(512); const terrain_heights = Buffer.alloc(512), terrain_blocks = Buffer.alloc(512); const fluid_heights = Buffer.alloc(512), fluid_blocks = Buffer.alloc(512), fluid_levels = Buffer.alloc(512); const spike_heights = Buffer.alloc(512), spike_blocks = Buffer.alloc(512); const segments: SpikeSegment[] = []; const feature_heights = Buffer.alloc(512), feature_blocks = Buffer.alloc(512); const features: NonNullable['entries'] = []; const palette: WorldChunk['palette'] = []; const indices = new Map(); const palette_index = (name: string) => { let index = indices.get(name); if (index === undefined) { index = palette.length; if (index >= 512) throw new ForwardFailure('INTERNAL_ERROR'); indices.set(name, index); palette.push({name, color: block_color(name.replace(/^minecraft:/, ''))}); } return index; }; const position = {x: 0, y: 0, z: 0}; const reader = entry.column.getBlockStateId; const read = reader.length >= 3 ? () => (reader as unknown as (x: number, y: number, z: number) => number).call(entry.column, position.x, position.y, position.z) : () => reader.call(entry.column, position); let reads = 0, has_fluid = false; for (let local_z = 0; local_z < 16; local_z++) { for (let local_x = 0; local_x < 16; local_x++) { let name = 'air', height = 0; let terrain_name = 'air', terrain_height = 0, fluid_name = '', level = 0; let terrain_state = -1; let spike_base_name = 'air', spike_base_height = 0; const column_segments: SpikeSegment[] = []; const index = local_z * 16 + local_x; position.x = local_x; position.z = local_z; for (let y = metadata.minY + metadata.height - 1; y >= metadata.minY; y--) { position.y = y; const state = read(); const block = this.bot.registry.blocksByStateId[state]; // Missing registry data is unknown, not evidence that this cell is air. if (!Number.isSafeInteger(state) || !block || typeof block.name !== 'string' || !block.name.length || block.name.length > 128) throw new ForwardFailure('INTERNAL_ERROR'); if (++reads % 4096 === 0) { await yield_turn(); check(); } const technical_name = block.name.replace(/^minecraft:/, ''); const fluid = technical_name === 'lava' ? 'lava' : water_blocks.has(technical_name) ? 'water' : ''; if (column_segments.length) { if (technical_name !== 'pointed_dripstone') { if (!air.has(technical_name) && !fluid) { spike_base_name = block.name; spike_base_height = y - metadata.minY + 1; } break; } // Commit only complete runs, so a capped column keeps its original surface. if (column_segments.length >= 16 || segments.length + column_segments.length >= 512) { column_segments.length = 0; break; } column_segments.push({index, height: y - metadata.minY + 1, ...spike_properties(state, block)}); continue; } if (!height) { if (air.has(technical_name)) continue; name = fluid || block.name; height = y - metadata.minY + 1; if (fluid) { fluid_name = fluid; has_fluid = true; level = technical_name === 'water' || technical_name === 'lava' ? fluid_level(state, block) : 0; continue; } } else if (fluid) { if (fluid !== fluid_name) break; continue; } // Air ends a contiguous fluid column without inventing a floor beneath the gap. if (!air.has(technical_name)) { terrain_name = block.name; terrain_height = y - metadata.minY + 1; terrain_state = state; } if (technical_name === 'pointed_dripstone' && segments.length < 512) { column_segments.push({index, height: y - metadata.minY + 1, ...spike_properties(state, block)}); continue; } break; } const offset = index * 2; heights.writeUInt16LE(height, offset); blocks.writeUInt16LE(palette_index(name), offset); terrain_heights.writeUInt16LE(terrain_height, offset); terrain_blocks.writeUInt16LE(palette_index(terrain_name), offset); spike_heights.writeUInt16LE(column_segments.length ? spike_base_height : terrain_height, offset); spike_blocks.writeUInt16LE(palette_index(column_segments.length ? spike_base_name : terrain_name), offset); segments.push(...column_segments); // Retain small surface structures separately from the solid heightfield. let base_height = column_segments.length ? spike_base_height : terrain_height; let base_name = column_segments.length ? spike_base_name : terrain_name; const terrain_block = this.bot.registry.blocksByStateId[terrain_state]; if (!column_segments.length && terrain_height && terrain_block && surface_feature(terrain_name.replace(/^minecraft:/, ''), surface_properties(terrain_state, terrain_block))) { const entries: typeof features = []; let remaining = 24, complete = false; for (let y = metadata.minY + terrain_height - 1; y >= metadata.minY && remaining-- > 0; y--) { position.y = y; const state = read(), block = this.bot.registry.blocksByStateId[state]; if (!Number.isSafeInteger(state) || !block || typeof block.name !== 'string' || !block.name.length || block.name.length > 128) throw new ForwardFailure('INTERNAL_ERROR'); if (++reads % 4096 === 0) { await yield_turn(); check(); } const technical = block.name.replace(/^minecraft:/, ''), properties = surface_properties(state, block); if (surface_feature(technical, properties)) { if (entries.length >= 16 || features.length + entries.length >= 512) break; entries.push({index, height: y - metadata.minY + 1, block: palette_index(block.name), properties}); } else { if (!entries.length) break; base_height = air.has(technical) || water_blocks.has(technical) || technical === 'lava' ? 0 : y - metadata.minY + 1; base_name = base_height ? block.name : 'air'; complete = true; break; } if (y === metadata.minY) { base_height = 0; base_name = 'air'; complete = true; } } if (complete) features.push(...entries); else { base_height = terrain_height; base_name = terrain_name; } } feature_heights.writeUInt16LE(base_height, offset); feature_blocks.writeUInt16LE(palette_index(base_name), offset); if (fluid_name) { fluid_heights.writeUInt16LE(height, offset); fluid_blocks.writeUInt16LE(palette_index(fluid_name), offset); fluid_levels.writeUInt16LE(level, offset); } } if (local_z % 2 === 1) { await yield_turn(); check(); } } return {x, z, revision: entry.revision, palette, heights: heights.toString('base64'), blocks: blocks.toString('base64'), ...(has_fluid ? { terrain: {heights: terrain_heights.toString('base64'), blocks: terrain_blocks.toString('base64')}, fluids: {heights: fluid_heights.toString('base64'), blocks: fluid_blocks.toString('base64'), levels: fluid_levels.toString('base64')}, } : {}), ...(segments.length ? {spikes: {heights: spike_heights.toString('base64'), blocks: spike_blocks.toString('base64'), segments}} : {}), ...(features.length ? {features: {heights: feature_heights.toString('base64'), blocks: feature_blocks.toString('base64'), entries: features}} : {})}; } dispose() { if (this.disposed) return; this.disposed = true; this.watches.clear(); this.idle(); } }