feat: add world voxel snapshot forwarding

Collect chunk voxel data from the bot world and stream snapshot,
chunk, and unwatch messages through the forward protocol.
This commit is contained in:
2026-10-23 10:44:37 +08:00
parent 0acef64f48
commit 6c69afb38a
11 changed files with 1997 additions and 18 deletions
+616
View File
@@ -0,0 +1,616 @@
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<LoadedWorld> & {sync?: LoadedWorld};
game: {minY: number; height: number; dimension: string};
entity: {position: Position; yaw: number};
registry: {blocksByStateId: Record<number, {name: string; minStateId?: number; maxStateId?: number;
boundingBox?: string; transparent?: boolean; filterLight?: number;
states?: Array<{name: string; type?: string; num_values: number; values?: unknown[]}>}>;
blocksByName?: Record<string, {minStateId?: number; defaultState?: number}>};
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<string, number> = {
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<WorldChunk['spikes']>['segments'][number];
function spike_properties(state: number, block: WorldBot['registry']['blocksByStateId'][number]) {
const result: Pick<SpikeSegment, 'direction' | 'thickness'> = {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<string, string | number | boolean> {
const result: Record<string, string | number | boolean> = {};
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<string, string | number | boolean>): 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<string, string | number | boolean> {
const result: Record<string, string | number | boolean> = {};
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<string, Watch>();
private readonly cache = new Map<string, ColumnCache>();
private readonly voxels = new Map<string, {entry: ColumnCache; chunk: WorldVoxelChunk; bytes: number}>();
private listeners: Array<[string, (...args: any[]) => void]> = [];
private expiry?: ReturnType<typeof setTimeout>;
private invalidation?: ReturnType<typeof setTimeout>;
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<Omit<WorldSnapshot, 'stream_id' | 'context_generation'>> {
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<WorldVoxelChunk> {
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<number, number>();
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<WorldPreview['metadata']>, check: () => void): Promise<WorldChunk> {
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<WorldChunk['features']>['entries'] = [];
const palette: WorldChunk['palette'] = [];
const indices = new Map<string, number>();
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();
}
}