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
+23
View File
@@ -678,3 +678,26 @@ test('context disposal reports offline once even when no instance was created',
assert.equal(await context.connect(), 467);
}
});
test('throwing log observers cannot interrupt socketClosed cleanup or offline reporting', async () => {
const {context, bots, statuses, managers, functions, timers} = setup();
await context.connect();
bots[0].emit('initialLogin');
const written = [];
context.logger.log = segments => written.push(...segments);
context.on('log', () => { throw new Error('log observer failed'); });
let lost = 0;
context.on('connection_lost', () => lost++);
assert.doesNotThrow(() => bots[0].emit('end', 'socketClosed'));
await flush();
assert.equal(context.bot_instance, null);
assert.equal(context.bot_status, 'offline');
assert.deepEqual(statuses.slice(-2), ['disconnecting', 'offline']);
assert.ok(written.includes('连接已断开: socketClosed'));
assert.ok(written.includes('[NODE] 机器人实例已关闭'));
assert.equal(managers[0].disposed, true);
assert.equal(functions[0].disposed, true);
assert.equal(lost, 1);
assert.equal(timers.size, 0);
await context.clear_this_context();
});
+18 -5
View File
@@ -484,7 +484,7 @@ test('execute_function traverses the relay with boolean results, permission chec
assert.equal(success_data(await ack(web.socket, 'relay_request', request('execute_function', payload))), false);
});
test('actual manager serves snapshots, logs and chat without forwarding bot events', async t => {
test('actual manager pushes lifecycle changes to snapshot readers without exposing other bot events', async t => {
const f = await real_fixture(t); const s = scope(); await f.open(s);
const manager = f.manager(s); const cmd = f.cmd(s);
assert.deepEqual(await manager.ensure(cmd), success({...s, node_ready: true, lease_expires_at: cmd.expires_at}));
@@ -497,21 +497,29 @@ test('actual manager serves snapshots, logs and chat without forwarding bot even
assert.deepEqual(Object.keys(absent).sort(), ['bot_id', 'bot_status', 'captured_at', 'context_exists', 'context_generation', 'seq', 'stream_id']);
assert.equal(absent.context_exists, false); assert.equal(absent.context_generation, null); assert.equal(absent.bot_status, 'offline');
code(await ack(web.socket, 'relay_request', request('bot.chat', {message: 'hello'})), 'NO_CONTEXT');
let leaks = 0; other.socket.on('relay_request', () => leaks++); other.socket.on('bot_event', () => leaks++);
let leaks = 0; other.socket.on('relay_request', () => leaks++);
const reader_events = []; other.socket.on('bot_event', event => reader_events.push(event));
assert.equal((await ack(web.socket, 'relay_request', request('bot.subscribe', {events: ['status_update', 'log', 'message', 'bot_error']}))).status, 'success');
const events = []; web.socket.on('bot_event', value => events.push(value));
const bot = online_context();
const initial_status = received(other.socket, 'bot_event', event => event.payload.status === 'online');
f.source(s.bot_id).set(bot);
await initial_status;
const snapshot = success_data(await ack(web.socket, 'relay_request', request()));
assert.equal(snapshot.context_exists, true); assert.equal(snapshot.bot_status, 'online');
assert.equal(snapshot.stream_id, absent.stream_id); assert.ok(snapshot.context_generation);
for (const state of ['offline', 'ready', 'connecting', 'online', 'disconnecting', 'reconnecting']) {
const pushed = received(web.socket, 'bot_event', event => event.payload.status === state);
const reader_pushed = received(other.socket, 'bot_event', event => event.payload.status === state);
bot.bot_status = state; bot.emit('status_update', state);
await Promise.all([pushed, reader_pushed]);
const current = success_data(await ack(web.socket, 'relay_request', request()));
assert.equal(current.context_exists, true);
assert.equal(current.bot_status, state); assert.equal(current.context_generation, snapshot.context_generation);
}
const back_online = received(web.socket, 'bot_event', event => event.payload.status === 'online');
bot.bot_status = 'online'; bot.emit('status_update', 'online');
await back_online;
const log_packet = received(web.socket, 'bot_log');
const metadata = {hoverEvent: {action: 'show_text', value: '悬停说明'},
clickEvent: {action: 'suggest_command', value: '/help'}, insertion: 'Alex'};
@@ -555,8 +563,9 @@ test('actual manager serves snapshots, logs and chat without forwarding bot even
bot.emit('error', new Error('SECRET stack'));
assert.equal(JSON.stringify({snapshot, logs}).includes('SECRET'), false);
await ack(other.socket, 'relay_request', request()); assert.equal(leaks, 0);
assert.deepEqual(events, []);
assert.equal(f.packets.some(packet => packet.event === 'bot_event'), false);
assert.ok(events.length >= 7 && reader_events.length >= 7);
assert.ok([...events, ...reader_events].every(event => event.name === 'status_update'));
assert.ok(f.packets.some(packet => packet.event === 'bot_event'));
manager.stop(); assert.equal(f.source(s.bot_id).count, 0); assert.equal(bot.listenerCount('log'), 0);
});
@@ -626,16 +635,20 @@ test('one Node manager isolates multiple Bot sessions and observes stop/start wi
await ack(web_a.socket, 'relay_request', request('bot.chat', {message: 'a'}));
await ack(web_b.socket, 'relay_request', request('bot.chat', {message: 'b'}));
assert.deepEqual(bot_a.sent, ['a']); assert.deepEqual(bot_b.sent, ['b']);
const stopped = received(web_a.socket, 'bot_event', event => event.payload.status === 'offline');
f.source(a.bot_id).set(undefined);
await stopped;
const absent = success_data(await ack(web_a.socket, 'relay_request', request()));
assert.equal(absent.bot_status, 'offline'); assert.equal(absent.context_generation, null);
const replacement = online_context();
const started = received(web_a.socket, 'bot_event', event => event.payload.status === 'online');
f.source(a.bot_id).set(replacement);
await started;
const after = success_data(await ack(web_a.socket, 'relay_request', request()));
assert.equal(after.stream_id, before.stream_id); assert.notEqual(after.context_generation, before.context_generation);
assert.equal(web_a.socket.connected, true); assert.equal(f.node_sockets.length, 2);
assert.equal(events_b.length, 0);
assert.equal(events_a.length, 0);
assert.deepEqual(events_a.map(event => event.payload.status), ['offline', 'online']);
assert.equal(bot_a.listenerCount('log'), 0);
});
+30 -4
View File
@@ -97,6 +97,27 @@ function setup() {
ensure(value = initial) { active = value; return manager.ensure(value); }};
}
test('lifecycle updates use reliable bot_event even while volatile logs are saturated', async t => {
const f = setup(); t.after(() => f.manager.stop());
await f.ensure();
const socket = f.sockets[0], stream = f.streams[0], received = [];
socket.on('bot_event', event => received.push(event));
socket.volatile = {emit() {}};
const base = {bot_id: 2, context_generation: randomUUID(), stream_id: randomUUID(), emitted_at: Date.now()};
for (let seq = 1; seq <= 160; seq++) stream.event({...base, seq, name: 'log', payload: {records: []}});
for (const [index, status] of ['online', 'disconnecting', 'offline'].entries()) {
stream.event({...base, seq: 161 + index, name: 'status_update', payload: {status, context_exists: true}});
}
assert.deepEqual(received.map(event => event.payload.status), ['online', 'disconnecting', 'offline']);
assert.ok(received.every(event => event.forward_session_id === f.command.forward_session_id));
stream.event({...base, seq: 165, name: 'message', payload: {text: 'not a lifecycle event'}});
assert.equal(received.length, 3);
socket.io.engine.writeBuffer.length = 64;
stream.event({...base, seq: 166, name: 'status_update', payload: {status: 'offline', context_exists: true}});
assert.equal(socket.connected, false);
assert.equal(stream.listeners, 0);
})
const tick = () => new Promise(resolve => setImmediate(resolve));
const code = response => response.data.code;
@@ -299,7 +320,7 @@ test('lease expiry retires data channel and queue capacity stays bounded under s
assert.equal(stalled.sockets.length, 1);
});
test('bot events are never forwarded while connected, congested or disconnected', async t => {
test('lifecycle events are forwarded while ready and congestion closes the stale transport', async t => {
const app = setup();
t.after(() => app.manager.stop());
await app.ensure();
@@ -308,23 +329,28 @@ test('bot events are never forwarded while connected, congested or disconnected'
let resyncs = 0;
stream.resync = () => { resyncs++; };
const event = {name: 'status_update', seq: 1, payload: {status: 'offline', context_exists: false}};
let lifecycle = 0;
socket.on('bot_event', () => lifecycle++);
stream.event(event);
assert.equal(socket.sent.filter(([name]) => name === 'bot_event').length, 0);
assert.equal(lifecycle, 1);
assert.equal(socket.sent.filter(([name]) => name === 'bot_log').length, 0);
stream.event({name: 'log', seq: 2, payload: {records: []}});
assert.equal(socket.sent.filter(([name]) => name === 'bot_log').length, 1);
socket.emit('relay_status', {forward_session_id: app.command.forward_session_id, node_ready: true, reason: 'resync_required'});
assert.equal(resyncs, 1);
socket.io.engine.writeBuffer.length = 64;
stream.event({...event, seq: 2});
stream.event({name: 'log', seq: 3, payload: {records: []}});
assert.equal(socket.sent.filter(([name]) => name === 'bot_log').length, 1);
assert.equal(socket.sent.filter(([name]) => name === 'bot_event').length, 0);
assert.equal(lifecycle, 1);
assert.equal(socket.connected, false);
socket.emit('relay_status', {forward_session_id: app.command.forward_session_id, node_ready: true, reason: 'resync_required'});
assert.equal(resyncs, 1);
socket.disconnect();
stream.event({...event, seq: 3});
stream.event({name: 'log', seq: 4, payload: {records: []}});
assert.equal(socket.sent.filter(([name]) => name === 'bot_log').length, 1);
assert.equal(socket.sent.filter(([name]) => name === 'bot_event').length, 0);
assert.equal(lifecycle, 1);
assert.equal(stream.listeners, 0);
});
+829
View File
@@ -0,0 +1,829 @@
import assert from 'node:assert/strict';
import {randomUUID} from 'node:crypto';
import {EventEmitter} from 'node:events';
import {createRequire, isBuiltin} from 'node:module';
import {fileURLToPath} from 'node:url';
import {setImmediate as tick, setTimeout as delay} from 'node:timers/promises';
import test from 'node:test';
import {build} from 'esbuild';
const root = fileURLToPath(new URL('../', import.meta.url));
const compiled = await build({
stdin: {contents: `export * from './bot/forward/stream.ts'; export * from './bot/forward/manager.ts';
export * from './utils/forward_protocol.ts';`, resolveDir: root, loader: 'ts'},
bundle: true, platform: 'node', format: 'cjs', write: false, metafile: true,
plugins: [{name: 'isolated-node', setup(build) {
build.onResolve({filter: /(?:instance\/index\.ts|^socket\.io-client$|console_print\.ts|path_utils\.ts)$/}, args =>
({path: args.path.includes('instance/') ? 'registry' : args.path === 'socket.io-client' ? 'socket'
: args.path.endsWith('path_utils.ts') ? 'path' : 'console', namespace: 'stub'}));
build.onLoad({filter: /.*/, namespace: 'stub'}, ({path}) => ({loader: 'js', contents: {
registry: 'export const get_bot_context = () => undefined; export const observe_bot_context = () => () => {};',
socket: 'export const io = () => {throw Error("Unexpected real socket")};',
console: 'export const console_print = {success: x => x, log() {}};',
path: 'export const get_project_root_path = () => {throw Error("Unexpected filesystem access")};',
}[path]}));
}}],
});
assert.ok(Object.keys(compiled.metafile.inputs).every(path => !path.includes('forward_server')));
const require = createRequire(import.meta.url);
const module = {exports: {}};
new Function('require', 'module', 'exports', compiled.outputFiles[0].text)(specifier => {
assert.ok(isBuiltin(specifier), `Unexpected external import: ${specifier}`);
return require(specifier);
}, module, module.exports);
const {BotStream, NodeForwardManager, validate_rpc} = module.exports;
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const code = expected => error => error.code === expected;
const request = (method, payload, forward_session_id = randomUUID(), deadline_at = Date.now() + 2000) => ({
request_id: randomUUID(), correlation_id: randomUUID(), forward_session_id, deadline_at, method, payload,
});
function fixture(t, {minY = -4, height = 16, direct = false} = {}) {
let now = 100;
let reads = 0;
const columns = new Map();
const bot = Object.assign(new EventEmitter(), {
chat() {}, version: '1.21.4', game: {minY, height, dimension: 'overworld'},
registry: {blocksByStateId: {7: {name: 'air'}, 8: {name: 'cave_air'}, 9: {name: 'void_air'},
101: {name: 'grass_block'}, 202: {name: 'water'}, 303: {name: 'stone'}, 404: {name: 'oak_leaves'},
505: {name: 'modded_marble'}}},
entity: {position: {x: -0.25, y: 3.5, z: -16.25}, yaw: Math.PI / 3},
});
const sync = {getColumn(x, z) { return columns.get(`${x},${z}`); }};
bot.world = direct ? sync : {sync};
const context = Object.assign(new EventEmitter(), {bot_status: 'online', bot_instance: bot});
let current = context;
const observers = new Set();
const source = {get: () => current, observe(_id, callback) { observers.add(callback); return () => observers.delete(callback); }};
const stream = new BotStream(1, {source, now: () => now});
t.after(() => stream.dispose());
const column = (surface = () => 7) => ({getBlockStateId(position) {
assert.equal(typeof position, 'object');
assert.ok(position.x >= 0 && position.x < 16 && position.z >= 0 && position.z < 16);
assert.ok(position.y >= minY && position.y < minY + height);
reads++;
return surface(position.x, position.y, position.z);
}});
return {bot, context, stream, columns, column, observers,
get reads() { return reads; },
snapshot(input = {}, check = () => {}) { return stream.world_snapshot({radius: 1, known: [], ...input}, now + 2000, check); },
time(value) { now = value; },
set(value) { current = value; for (const observer of observers) observer(value); },
};
}
function decoded(chunk, layer = chunk) {
for (const name of ['heights', 'blocks', ...('levels' in layer ? ['levels'] : [])]) {
assert.equal(layer[name].length, 684);
assert.equal(Buffer.from(layer[name], 'base64').length, 512);
assert.equal(Buffer.from(layer[name], 'base64').toString('base64'), layer[name]);
}
const heights = Buffer.from(layer.heights, 'base64'), blocks = Buffer.from(layer.blocks, 'base64');
const levels = layer.levels && Buffer.from(layer.levels, 'base64');
const result = [];
for (let index = 0; index < 256; index++) {
const palette_index = blocks.readUInt16LE(index * 2);
assert.ok(palette_index < chunk.palette.length);
const cell = {height: heights.readUInt16LE(index * 2), ...chunk.palette[palette_index]};
if (levels) {
cell.level = levels.readUInt16LE(index * 2);
assert.ok(cell.level >= 0 && cell.level <= 15);
if (cell.height) assert.ok(['water', 'lava'].includes(cell.name));
else assert.equal(cell.level, 0);
}
result.push(cell);
}
assert.ok(chunk.palette.length >= 1 && chunk.palette.length <= 512);
for (const entry of chunk.palette) {
assert.deepEqual(Object.keys(entry).sort(), ['color', 'name']);
assert.ok(entry.name.length <= 128);
assert.ok(Number.isInteger(entry.color) && entry.color >= 0 && entry.color <= 0xffffff);
}
return result;
}
test('surface feature sampling retains torch, sign, hanging and waterlogged states above their foundation', async t => {
const h = fixture(t), mf = createRequire(require.resolve('@wp2508/mineflayer'));
const data = mf('minecraft-data')('1.21.4');
const id = (name, properties) => {
const b = data.blocksByName[name]; let offset = 0;
for (const p of b.states) offset = offset * p.num_values + (p.type === 'bool' ? properties[p.name] === true ? 0 : 1 : p.values.indexOf(String(properties[p.name])));
const value = b.minStateId + offset; h.bot.registry.blocksByStateId[value] = b; return value;
};
const states = [id('wall_torch', {facing: 'east'}), id('oak_sign', {rotation: 7, waterlogged: false}),
id('oak_hanging_sign', {rotation: 3, attached: true, waterlogged: true}),
id('oak_wall_hanging_sign', {facing: 'north', waterlogged: false}), id('oak_slab', {type: 'bottom', waterlogged: true}),
id('oak_stairs', {facing: 'south', half: 'top', shape: 'inner_left', waterlogged: true})];
h.columns.set('-1,-2', h.column((x,y,z) => z === 0 && x < states.length ? y === 3 ? states[x] : y === 2 ? 303 : 7 : y === 2 ? 303 : 7));
const result = await h.snapshot(), chunk = result.chunks[0];
assert.equal(chunk.features.entries.length, states.length);
const entries = chunk.features.entries;
assert.deepEqual(entries[0].properties, {facing: 'east'});
assert.equal(entries[1].properties.rotation, 7); assert.equal(entries[1].properties.waterlogged, false);
assert.equal(entries[2].properties.attached, true); assert.equal(entries[2].properties.waterlogged, true);
assert.equal(entries[4].properties.type, 'bottom'); assert.equal(entries[5].properties.shape, 'inner_left');
assert.ok(entries.every(e => e.height === 8));
assert.ok(decoded(chunk, chunk.features).slice(0, states.length).every(c => c.height === 7 && c.name === 'stone'));
});
test('feature sampling retains short runs and falls back without holes when a column exceeds its bound', async t => {
const h = fixture(t, {minY: 0, height: 24});
h.bot.registry.blocksByStateId[700] = {name: 'oak_sign', minStateId: 700, states: [{name: 'rotation', num_values: 16, values: Array.from({length: 16}, (_, i) => String(i))}]};
h.columns.set('-1,-2', h.column((x,y,z) => {
if (z !== 0 || x > 1) return y === 1 ? 303 : 7;
if (x === 0) return y === 3 || y === 2 ? 700 : y === 1 ? 303 : 7;
return y >= 2 && y <= 18 ? 700 : y === 1 ? 303 : 7;
}));
const chunk = (await h.snapshot()).chunks[0];
assert.equal(chunk.features.entries.length, 2);
assert.ok(chunk.features.entries.every(e => e.index === 0));
const bases = decoded(chunk, chunk.features);
assert.equal(bases[0].height, 2);
assert.equal(bases[1].height, 19);
assert.equal(bases[1].name, 'oak_sign');
});
test('world protocol validates exact bounded requests, revisions and descriptors', () => {
const valid = {radius: 3, known: [{x: -1, z: -2, revision: randomUUID()}], world_generation: randomUUID(), watch_id: randomUUID()};
assert.equal(validate_rpc(request('world.snapshot', valid)).payload, valid);
assert.equal(validate_rpc(request('world.unwatch', {watch_id: valid.watch_id})).method, 'world.unwatch');
for (const payload of [null, {}, [], {radius: 1}, {known: []}, {...valid, extra: true},
...[0, 4, -1, 1.5, NaN, Infinity, '1', null].map(radius => ({...valid, radius})),
{...valid, known: Array.from({length: 50}, (_, x) => ({x, z: 0, revision: randomUUID()}))},
{...valid, known: Array(1)}, {...valid, known: [{x: 0, z: 0}]},
{...valid, known: [{x: 1.2, z: 0, revision: randomUUID()}]},
{...valid, known: [{x: 0, z: Infinity, revision: randomUUID()}]},
{...valid, known: [{x: 0, z: 0, revision: 'bad'}]},
{...valid, known: [{x: 0, z: 0, revision: randomUUID(), extra: true}]},
{...valid, known: [valid.known[0], valid.known[0]]},
{...valid, world_generation: undefined}, {...valid, watch_id: 'bad'}])
assert.throws(() => validate_rpc(request('world.snapshot', payload)), code('INVALID_REQUEST'));
for (const payload of [{}, {watch_id: 'bad'}, {watch_id: valid.watch_id, extra: 1}, {watch_id: null}])
assert.throws(() => validate_rpc(request('world.unwatch', payload)), code('INVALID_REQUEST'));
for (const name of ['radius', 'known', 'watch_id', 'world_generation']) {
let reads = 0;
const payload = Object.defineProperty({...valid}, name, {enumerable: true, get() { reads++; return valid[name]; }});
assert.throws(() => validate_rpc(request('world.snapshot', payload)), code('INVALID_REQUEST'));
assert.equal(reads, 0);
}
let reads = 0;
const known = [];
Object.defineProperty(known, '0', {enumerable: true, get() { reads++; return valid.known[0]; }});
assert.throws(() => validate_rpc(request('world.snapshot', {...valid, known})), code('INVALID_REQUEST'));
assert.equal(reads, 0);
});
test('snapshot encodes negative chunk coordinates, z-major heights and runtime registry surface names', async t => {
const app = fixture(t);
app.columns.set('-1,-2', app.column((x, y, z) => {
if (x === 1 && z === 0) return y === -1 ? 202 : 7;
if (x === 2 && z === 0) return y === 10 ? 404 : 7;
if (x === 3 && z === 0) return y === -4 ? 303 : 7;
if (x === 4 && z === 0) return y === 2 ? 505 : 7;
if (x === 0 && z === 1) return y > 4 ? (y % 2 ? 8 : 9) : y === 4 ? 101 : 7;
return 7;
}));
const result = await app.snapshot();
assert.deepEqual(Object.keys(result).sort(), ['world_generation', 'context_generation', 'stream_id', 'watch_id', 'version',
'dimension', 'min_y', 'height', 'center', 'position', 'chunks', 'loaded', 'partial'].sort());
for (const key of ['world_generation', 'context_generation', 'stream_id', 'watch_id']) assert.match(result[key], uuid);
assert.deepEqual(result.center, {x: -1, z: -2});
assert.deepEqual(result.position, {x: -0.25, y: 3.5, z: -16.25, yaw: Math.PI / 3});
assert.equal(result.min_y, -4); assert.equal(result.height, 16);
assert.equal(result.version, app.bot.version); assert.equal(result.dimension, 'overworld');
assert.equal(result.partial, true); assert.equal(result.chunks.length, 1); assert.equal(result.loaded.length, 1);
const chunk = result.chunks[0];
assert.deepEqual(result.loaded, [{x: -1, z: -2, revision: chunk.revision}]);
const cells = decoded(chunk);
assert.deepEqual(cells[0], {height: 0, name: 'air', color: 0});
assert.deepEqual(cells[1], {height: 4, name: 'water', color: 0x3f76e4});
assert.equal(cells[2].height, 15); assert.equal(cells[2].name, 'oak_leaves');
assert.equal(cells[3].height, 1); assert.equal(cells[3].name, 'stone');
assert.equal(cells[4].height, 7); assert.equal(cells[4].name, 'modded_marble');
assert.equal(cells[16].height, 9); assert.equal(cells[16].name, 'grass_block');
assert.notEqual(cells[1].color, cells[16].color);
});
test('only loaded columns are returned, zero-chunk metadata remains valid and radius 3 is bounded at 49', async t => {
const app = fixture(t, {direct: true});
const empty = await app.snapshot();
assert.deepEqual(empty.chunks, []); assert.deepEqual(empty.loaded, []); assert.equal(empty.partial, true);
for (let z = -5; z <= 1; z++) for (let x = -4; x <= 2; x++) app.columns.set(`${x},${z}`, app.column(() => 303));
const full = await app.snapshot({radius: 3, watch_id: empty.watch_id});
assert.equal(full.chunks.length, 49); assert.equal(full.loaded.length, 49); assert.equal(full.partial, false);
const cached = await app.snapshot({radius: 3, watch_id: full.watch_id, world_generation: full.world_generation, known: full.loaded});
assert.equal(cached.chunks.length, 0); assert.equal(cached.loaded.length, 49); assert.equal(cached.partial, false);
});
test('matching generation omits unchanged chunks without reads, block events change revisions before hint', async t => {
const app = fixture(t);
let state = 101;
app.columns.set('-1,-2', app.column((_x, y) => y === 0 ? state : 7));
app.columns.set('0,-2', app.column(() => 303));
const first = await app.snapshot();
const reads = app.reads;
const input = {known: first.loaded, world_generation: first.world_generation, watch_id: first.watch_id};
const same = await app.snapshot(input);
assert.equal(same.chunks.length, 0); assert.equal(same.watch_id, first.watch_id); assert.equal(app.reads, reads);
state = 202;
app.bot.emit('blockUpdate', null, {position: {x: -1, y: 0, z: -17}});
const changed = await app.snapshot(input);
assert.equal(changed.world_generation, first.world_generation);
assert.equal(changed.chunks.length, 1);
assert.equal(decoded(changed.chunks[0])[0].name, 'water');
assert.notEqual(changed.chunks[0].revision, first.chunks[0].revision);
const forced = await app.snapshot({...input, world_generation: randomUUID()});
assert.equal(forced.chunks.length, 2);
first.chunks[0].palette[0].name = 'mutated';
const clone = await app.snapshot({watch_id: first.watch_id});
assert.notEqual(clone.chunks[0].palette[0].name, 'mutated');
});
test('column replacement and unload cannot reuse stale cached surfaces', async t => {
const app = fixture(t);
app.columns.set('-1,-2', app.column(() => 101));
const first = await app.snapshot();
const input = {known: first.loaded, world_generation: first.world_generation, watch_id: first.watch_id};
app.columns.set('-1,-2', app.column(() => 303));
const replacement = await app.snapshot(input);
assert.notEqual(replacement.chunks[0].revision, first.loaded[0].revision);
assert.equal(decoded(replacement.chunks[0])[0].name, 'stone');
app.columns.delete('-1,-2');
app.bot.emit('chunkColumnUnload', {x: -16, z: -32});
const unloaded = await app.snapshot(input);
assert.deepEqual(unloaded.loaded, []); assert.deepEqual(unloaded.chunks, []);
app.columns.set('-1,-2', app.column(() => 202));
app.bot.emit('chunkColumnLoad', {x: -16, z: -32});
const reloaded = await app.snapshot(input);
assert.notEqual(reloaded.loaded[0].revision, first.loaded[0].revision);
});
test('world object, dimension, version and explicit same-world respawn reset the generation', async t => {
const app = fixture(t);
app.columns.set('-1,-2', app.column(() => 303));
let last = await app.snapshot();
for (const reset of [() => { app.bot.world = {sync: app.bot.world.sync}; },
() => { app.bot.game.dimension = 'the_nether'; }, () => { app.bot.version = '1.20.4'; },
() => { app.bot.emit('respawn'); }]) {
reset();
const next = await app.snapshot({watch_id: last.watch_id, world_generation: last.world_generation, known: last.loaded});
assert.notEqual(next.world_generation, last.world_generation);
assert.equal(next.chunks.length, 1);
assert.equal(next.watch_id, last.watch_id);
last = next;
}
});
test('watch events coalesce at 200ms, filter by active radius and only invalidate on chunk movement', async t => {
const app = fixture(t);
const events = [];
app.stream.subscribe(event => { if (event.name === 'info_update') events.push(event); });
assert.equal(app.bot.listenerCount('chunkColumnLoad'), 0);
const first = await app.snapshot();
assert.equal(app.bot.listenerCount('chunkColumnLoad'), 1);
for (let index = 0; index < 20; index++) app.bot.emit('blockUpdate', null, {position: {x: -1, z: -17}});
app.bot.emit('chunkColumnLoad', {x: 0, z: -32});
assert.equal(events.length, 0);
await delay(230);
assert.equal(events.length, 1);
assert.deepEqual(events[0].payload, {name: 'world_preview', value: {world_generation: first.world_generation}});
app.bot.emit('chunkColumnLoad', {x: 1600, z: 1600});
app.bot.entity.position.x = -0.5; app.bot.emit('move');
await delay(230);
assert.equal(events.length, 1);
app.bot.entity.position.x = 0.1; app.bot.emit('move');
await delay(230);
assert.equal(events.length, 2);
assert.deepEqual(app.stream.world_unwatch({watch_id: first.watch_id}, 2000), {released: true});
assert.deepEqual(app.stream.world_unwatch({watch_id: first.watch_id}, 2000), {released: true});
for (const name of ['chunkColumnLoad', 'chunkColumnUnload', 'blockUpdate', 'move', 'respawn', 'end'])
assert.equal(app.bot.listenerCount(name), 0);
});
test('leases cap at 64, renew for 45 seconds and final unwatch or TTL removes caches, timers and listeners', async t => {
const app = fixture(t);
app.columns.set('-1,-2', app.column(() => 303));
const first = await app.snapshot();
const ids = [first.watch_id];
for (let index = 1; index < 64; index++) ids.push((await app.snapshot()).watch_id);
await assert.rejects(app.snapshot(), code('RATE_LIMITED'));
app.time(44_000);
const renewed = await app.snapshot({watch_id: first.watch_id, known: first.loaded, world_generation: first.world_generation});
assert.equal(renewed.watch_id, first.watch_id);
assert.equal(renewed.chunks.length, 0);
app.time(45_101);
for (const id of ids.slice(1)) app.stream.world_unwatch({watch_id: id}, 90_000);
assert.equal(app.bot.listenerCount('blockUpdate'), 1);
app.time(89_001);
app.bot.emit('move');
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
const reads = app.reads;
const fresh = await app.snapshot({watch_id: first.watch_id, known: first.loaded, world_generation: first.world_generation});
assert.notEqual(fresh.watch_id, first.watch_id); assert.notEqual(fresh.world_generation, first.world_generation);
assert.ok(app.reads > reads);
app.bot.emit('chunkColumnLoad', {x: -16, z: -32});
app.stream.world_unwatch({watch_id: fresh.watch_id}, 100_000);
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
});
test('lease expiry timer detaches without further bot traffic and cancels a queued invalidation', async t => {
t.mock.timers.enable({apis: ['setTimeout', 'Date'], now: 1000});
const app = fixture(t);
app.time(1000);
const events = [];
app.stream.subscribe(event => { if (event.name === 'info_update') events.push(event); });
const first = await app.snapshot();
app.time(45_900); t.mock.timers.tick(44_900);
app.bot.emit('chunkColumnLoad', {x: -16, z: -32});
app.time(46_000); t.mock.timers.tick(100);
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
t.mock.timers.tick(1000);
assert.equal(events.length, 0);
assert.deepEqual(app.stream.world_unwatch({watch_id: first.watch_id}, 47_000), {released: true});
});
test('watch radii use the union of active leases and shrink after releasing the wider watch', async t => {
t.mock.timers.enable({apis: ['setTimeout']});
const app = fixture(t);
const events = [];
app.stream.subscribe(event => { if (event.name === 'info_update') events.push(event); });
const narrow = await app.snapshot();
const wide = await app.snapshot({radius: 3});
app.bot.emit('chunkColumnLoad', {x: 32, z: -32});
t.mock.timers.tick(200);
assert.equal(events.length, 1);
app.stream.world_unwatch({watch_id: wide.watch_id}, 2000);
app.bot.emit('chunkColumnLoad', {x: 32, z: -32});
t.mock.timers.tick(200);
assert.equal(events.length, 1);
app.bot.emit('respawn');
t.mock.timers.tick(200);
assert.equal(events.length, 2);
const reset = await app.snapshot({watch_id: narrow.watch_id});
assert.equal(reset.world_generation, events[1].payload.value.world_generation);
});
test('caller mutation after dispatch cannot change radius or known revisions, and scans are bounded to four concurrent calls', async t => {
const app = fixture(t);
app.columns.set('-1,-2', app.column());
const input = {radius: 1, known: []};
const pending = app.stream.world_snapshot(input, 2100);
input.radius = 3; input.known.push({x: 0, z: 0, revision: 'bad'});
const result = await pending;
assert.equal(result.chunks.length, 1);
app.bot.emit('blockUpdate', null, {position: {x: -1, z: -17}});
const calls = Array.from({length: 4}, () => app.snapshot());
await assert.rejects(app.snapshot(), code('RATE_LIMITED'));
await Promise.all(calls);
});
test('scalar column adapters and deterministic fallback colors remain compatible with the surface contract', async t => {
const app = fixture(t);
app.columns.set('-1,-2', {getBlockStateId(x, y, z) {
assert.equal(typeof x, 'number'); assert.equal(typeof z, 'number');
return y === 3 ? 505 : 7;
}});
const first = await app.snapshot();
const cells = decoded(first.chunks[0]);
assert.equal(cells[0].name, 'modded_marble'); assert.equal(cells[0].height, 8);
assert.equal(Object.hasOwn(first.chunks[0], 'terrain'), false);
assert.equal(Object.hasOwn(first.chunks[0], 'fluids'), false);
assert.equal(Object.hasOwn(first.chunks[0], 'spikes'), false);
app.bot.emit('respawn');
const reset = await app.snapshot({watch_id: first.watch_id});
assert.equal(decoded(reset.chunks[0])[0].color, cells[0].color);
});
test('height 4096 and a full 256-entry surface palette fit UInt16, and transient metadata cannot retain stale revisions', async t => {
const app = fixture(t, {minY: -4096, height: 4096});
for (let index = 0; index < 256; index++) app.bot.registry.blocksByStateId[1000 + index] = {name: `surface_${index}`};
app.columns.set('-1,-2', app.column((x, _y, z) => 1000 + z * 16 + x));
const first = await app.snapshot();
const cells = decoded(first.chunks[0]);
assert.equal(first.chunks[0].palette.length, 256);
assert.equal(cells[255].name, 'surface_255'); assert.equal(cells[255].height, 4096);
app.bot.entity.yaw = NaN;
app.bot.emit('blockUpdate', null, {position: {x: -1, z: -17}});
app.bot.entity.yaw = 0;
const next = await app.snapshot({known: first.loaded, world_generation: first.world_generation, watch_id: first.watch_id});
assert.equal(next.chunks.length, 1); assert.notEqual(next.loaded[0].revision, first.loaded[0].revision);
});
test('surface reads yield and abort promptly on deadline or transport disconnection', async t => {
for (const cause of ['deadline', 'transport']) await t.test(cause, async t => {
const app = fixture(t, {minY: -64, height: 384});
app.columns.set('-1,-2', app.column());
let stale = false;
setImmediate(() => { if (cause === 'deadline') app.time(5000); else stale = true; });
await assert.rejects(app.snapshot({}, () => {
if (stale) throw Object.assign(new Error(), {code: 'SESSION_STALE'});
}), code(cause === 'deadline' ? 'TIMEOUT' : 'SESSION_STALE'));
assert.ok(app.reads <= 4096, `scan must stop during first read batch: ${app.reads}`);
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
});
});
test('in-flight reads reject context replacement, offline transition, disposal and column mutation', async t => {
for (const cause of ['context', 'instance', 'offline', 'dispose', 'block', 'replacement', 'unload', 'respawn', 'world', 'move'])
await t.test(cause, async t => {
const app = fixture(t);
app.columns.set('-1,-2', app.column());
setImmediate(() => {
if (cause === 'context') app.set(Object.assign(new EventEmitter(), {bot_status: 'online', bot_instance: app.bot}));
if (cause === 'instance') app.context.bot_instance = {...app.bot};
if (cause === 'offline') { app.context.bot_status = 'offline'; app.context.emit('status_update'); }
if (cause === 'dispose') app.stream.dispose();
if (cause === 'block') app.bot.emit('blockUpdate', null, {position: {x: -1, z: -17}});
if (cause === 'replacement') app.columns.set('-1,-2', app.column(() => 303));
if (cause === 'unload') app.columns.delete('-1,-2');
if (cause === 'respawn') app.bot.emit('respawn');
if (cause === 'world') app.bot.world = {sync: app.bot.world.sync};
if (cause === 'move') { app.bot.entity.position.x = 20; app.bot.emit('move'); }
});
await assert.rejects(app.snapshot(), error => ['CANCELLED', 'BOT_NOT_ONLINE'].includes(error.code));
app.stream.dispose();
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
});
});
test('offline, missing context, invalid metadata and unknown registry data never fabricate air', async t => {
const app = fixture(t);
await assert.rejects(app.stream.world_snapshot({radius: 0, known: []}, 200), code('INVALID_REQUEST'));
await assert.rejects(app.stream.world_snapshot({radius: 1, known: []}, 100), code('TIMEOUT'));
app.context.bot_status = 'offline';
await assert.rejects(app.snapshot(), code('BOT_NOT_ONLINE'));
assert.deepEqual(app.stream.world_unwatch({watch_id: randomUUID()}, 200), {released: true});
app.context.bot_status = 'online';
app.bot.entity.yaw = NaN;
await assert.rejects(app.snapshot(), code('INTERNAL_ERROR'));
app.bot.entity.yaw = 0;
app.columns.set('-1,-2', app.column(() => 999_999));
await assert.rejects(app.snapshot(), code('INTERNAL_ERROR'));
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
app.set(undefined);
await assert.rejects(app.snapshot(), code('NO_CONTEXT'));
assert.deepEqual(app.stream.world_unwatch({watch_id: randomUUID()}, 200), {released: true});
app.stream.dispose(); app.stream.dispose();
await assert.rejects(app.snapshot(), code('CANCELLED'));
assert.equal(app.observers.size, 0);
});
test('fluid columns retain top surfaces and sample contiguous water plants down to actual terrain', async t => {
const app = fixture(t);
const plants = ['bubble_column', 'kelp', 'kelp_plant', 'seagrass', 'tall_seagrass'];
plants.forEach((name, index) => { app.bot.registry.blocksByStateId[600 + index] = {name: `minecraft:${name}`}; });
app.bot.registry.blocksByStateId[700] = {name: 'minecraft:lava'};
app.columns.set('-1,-2', app.column((x, y, z) => {
if (z) return 7;
if (x === 0) return y >= -2 && y <= 2 ? 202 : y === -3 ? 303 : 7;
if (x >= 1 && x <= 5) return y === 4 ? 599 + x : y === 3 ? 602 : y === 2 ? 202 : y === 1 ? 101 : 7;
if (x === 6) return y === 6 || y === 5 ? 700 : y === 4 ? 505 : 7;
if (x === 7) return y > 8 ? 7 : y === 8 ? 404 : 999_999; // Solid tops must never inspect lower cells.
if (x === 8) return y === -4 ? 202 : 7;
if (x === 9) return y === 4 ? 202 : y === -4 ? 303 : 7;
if (x === 10 || x === 11) return y > 4 ? 7 : y === 4 ? (x === 10 ? 202 : 700)
: y === 3 ? (x === 10 ? 700 : 202) : 999_999;
return 7;
}));
const {chunks: [chunk]} = await app.snapshot();
const top = decoded(chunk), terrain = decoded(chunk, chunk.terrain), fluids = decoded(chunk, chunk.fluids);
assert.deepEqual(top[0], {height: 7, name: 'water', color: 0x3f76e4});
assert.deepEqual(terrain[0], {height: 2, name: 'stone', color: 0x858585});
assert.deepEqual(fluids[0], {...top[0], level: 0}); // Missing metadata defaults to source.
for (let x = 1; x <= 5; x++) {
assert.deepEqual(top[x], {height: 9, name: 'water', color: 0x3f76e4});
assert.deepEqual(fluids[x], {...top[x], level: 0});
assert.equal(terrain[x].name, 'grass_block'); assert.equal(terrain[x].height, 6);
}
assert.equal(top[6].name, 'lava'); assert.equal(fluids[6].name, 'lava'); assert.equal(fluids[6].level, 0);
assert.equal(fluids[6].height, 11); assert.equal(terrain[6].name, 'modded_marble'); assert.equal(terrain[6].height, 9);
assert.deepEqual(terrain[7], top[7]); assert.equal(fluids[7].height, 0);
assert.equal(fluids[8].height, 1); assert.equal(terrain[8].height, 0);
assert.equal(fluids[9].height, 9); assert.equal(terrain[9].height, 0); // No floor across an air gap.
for (const x of [10, 11]) {
assert.equal(top[x].name, x === 10 ? 'water' : 'lava');
assert.deepEqual(fluids[x], {...top[x], level: 0});
assert.equal(terrain[x].height, 0); // A different fluid ends the column without becoming solid terrain.
}
assert.equal(top[12].height, 0); assert.equal(terrain[12].height, 0); assert.equal(fluids[12].height, 0);
assert.ok(!chunk.palette.some(entry => plants.some(name => entry.name.endsWith(name))));
});
test('fluid level decoding respects registry property order and explicit level value order', async t => {
const app = fixture(t, {minY: -10, height: 4});
const states = [{name: 'before', num_values: 2},
{name: 'level', num_values: 4, values: ['8', '0', '15', '3']}, {name: 'after', num_values: 3}];
const expected = [8, 0, 15, 3];
expected.forEach((_level, index) => {
const state = 1000 + 12 + index * 3 + 2;
app.bot.registry.blocksByStateId[state] = {name: 'water', minStateId: 1000, states};
});
app.columns.set('-1,-2', app.column((x, y) => y === -7 && x < 4 ? 1000 + 12 + x * 3 + 2 : 303));
const {chunks: [chunk]} = await app.snapshot();
assert.deepEqual(decoded(chunk, chunk.fluids).slice(0, 4).map(cell => cell.level), expected);
assert.deepEqual(decoded(chunk, chunk.terrain).slice(0, 4).map(cell => cell.height), [3, 3, 3, 3]);
});
test('one shared palette accommodates 256 distinct floors plus water and lava', async t => {
const app = fixture(t, {minY: -64, height: 2});
app.bot.registry.blocksByStateId[600] = {name: 'lava'};
for (let index = 0; index < 256; index++) app.bot.registry.blocksByStateId[1000 + index] = {name: `floor_${index}`};
app.columns.set('-1,-2', app.column((x, y, z) => y === -63 ? (x % 2 ? 202 : 600) : 1000 + z * 16 + x));
const {chunks: [chunk]} = await app.snapshot();
const terrain = decoded(chunk, chunk.terrain), fluids = decoded(chunk, chunk.fluids);
assert.equal(chunk.palette.length, 258);
for (let index = 0; index < 256; index++) {
assert.equal(terrain[index].name, `floor_${index}`); assert.equal(terrain[index].height, 1);
assert.equal(fluids[index].name, index % 2 ? 'water' : 'lava'); assert.equal(fluids[index].height, 2);
}
});
test('fluid terrain scans reject unknown blocks and yield before cancellation without caching partial data', async t => {
await t.test('unknown beneath fluid', async t => {
const app = fixture(t);
let unknown = true;
app.columns.set('-1,-2', app.column((_x, y) => y === 11 ? 202 : unknown ? 999_999 : 303));
await assert.rejects(app.snapshot(), code('INTERNAL_ERROR'));
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
unknown = false;
const {chunks: [chunk]} = await app.snapshot();
assert.equal(decoded(chunk, chunk.terrain)[0].height, 15);
});
await t.test('deep fluid cancellation', async t => {
const app = fixture(t, {minY: -64, height: 4096});
app.columns.set('-1,-2', app.column(() => 202));
let stale = false;
setImmediate(() => { stale = true; });
await assert.rejects(app.snapshot({}, () => {
if (stale) throw Object.assign(new Error(), {code: 'SESSION_STALE'});
}), code('SESSION_STALE'));
assert.equal(app.reads, 4096);
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
app.columns.set('-1,-2', app.column(() => 303));
const {chunks: [chunk]} = await app.snapshot();
assert.equal(Object.hasOwn(chunk, 'terrain'), false);
assert.equal(Object.hasOwn(chunk, 'fluids'), false);
});
});
test('spike runs decode mixed-radix direction and every thickness while preserving original surfaces', async t => {
const app = fixture(t);
const thicknesses = ['base', 'middle', 'frustum', 'tip_merge', 'tip'];
const states = [{name: 'before', num_values: 2},
{name: 'vertical_direction', num_values: 2, values: ['down', 'up']},
{name: 'thickness', num_values: 5, values: thicknesses},
{name: 'waterlogged', num_values: 2, values: ['true', 'false']}, {name: 'after', num_values: 3}];
const state_id = (direction, thickness) => 1000 + (((2 + direction) * 5 + thickness) * 2 + 1) * 3 + 2;
for (let direction = 0; direction < 2; direction++) for (let thickness = 0; thickness < 5; thickness++)
app.bot.registry.blocksByStateId[state_id(direction, thickness)] = {name: 'minecraft:pointed_dripstone', minStateId: 1000, states};
app.columns.set('-1,-2', app.column((x, y, z) => {
if (z || x > 2) return y === 4 ? 101 : 7;
if (x === 2) return y === 9 ? 404 : y < 9 ? 999_999 : 7;
return y <= 7 && y >= 3 ? state_id(x, 7 - y) : y === 2 ? 303 : 7;
}));
const {chunks: [chunk]} = await app.snapshot();
const top = decoded(chunk), bases = decoded(chunk, chunk.spikes);
assert.equal(Object.hasOwn(chunk, 'terrain'), false);
assert.equal(Object.hasOwn(chunk, 'fluids'), false);
for (const x of [0, 1]) {
assert.equal(top[x].name, 'minecraft:pointed_dripstone'); assert.equal(top[x].height, 12);
assert.equal(bases[x].name, 'stone'); assert.equal(bases[x].height, 7);
}
for (let index = 2; index < 256; index++) assert.deepEqual(bases[index], top[index]);
assert.deepEqual(chunk.spikes.segments, [0, 1].flatMap(index => thicknesses.map((thickness, offset) =>
({index, height: 12 - offset, direction: index ? 'up' : 'down', thickness}))));
});
test('spike bases stop at air gaps and the world minimum, and absent metadata defaults to up/tip', async t => {
const app = fixture(t);
app.bot.registry.blocksByStateId[600] = {name: 'pointed_dripstone'};
app.columns.set('-1,-2', app.column((x, y, z) => {
if (z) return 7;
if (x === 0) return y === 4 || y === 3 ? 600 : y === -4 ? 303 : 7;
if (x === 1) return y === -4 ? 600 : 7;
if (x === 2) return y > 4 ? 7 : y === 4 ? 600 : y === 3 ? 8 : 999_999;
if (x === 3) return y > 4 ? 7 : y === 4 ? 600 : y === 3 ? 9 : 999_999;
return 7;
}));
const {chunks: [chunk]} = await app.snapshot();
const bases = decoded(chunk, chunk.spikes);
for (const x of [0, 1, 2, 3]) assert.deepEqual(bases[x], {height: 0, name: 'air', color: 0});
assert.deepEqual(chunk.spikes.segments, [
{index: 0, height: 9, direction: 'up', thickness: 'tip'},
{index: 0, height: 8, direction: 'up', thickness: 'tip'},
{index: 1, height: 1, direction: 'up', thickness: 'tip'},
{index: 2, height: 9, direction: 'up', thickness: 'tip'},
{index: 3, height: 9, direction: 'up', thickness: 'tip'},
]);
});
test('underwater spikes override only spike bases and mirror terrain in every other cell', async t => {
const app = fixture(t);
app.bot.registry.blocksByStateId[600] = {name: 'pointed_dripstone'};
app.bot.registry.blocksByStateId[601] = {name: 'lava'};
app.columns.set('-1,-2', app.column((x, y, z) => {
if (z) return y === 0 ? 101 : 7;
if (x <= 1) return y === 8 || y === 7 ? 202 : y === 6 || y === 5 ? 600 : y === 4 && !x ? 303 : 7;
if (x === 2) return y === 8 ? 202 : y === 7 ? 303 : 7;
if (x === 3) return y > 8 ? 7 : y === 8 ? 202 : y === 7 ? 601 : 999_999;
if (x === 4) return y > 8 ? 7 : y === 8 ? 404 : 999_999;
return 7;
}));
const {chunks: [chunk]} = await app.snapshot();
const top = decoded(chunk), terrain = decoded(chunk, chunk.terrain), fluids = decoded(chunk, chunk.fluids);
const bases = decoded(chunk, chunk.spikes);
for (const x of [0, 1]) {
assert.equal(top[x].name, 'water'); assert.equal(top[x].height, 13);
assert.equal(fluids[x].height, 13); assert.equal(fluids[x].level, 0);
assert.equal(terrain[x].name, 'pointed_dripstone'); assert.equal(terrain[x].height, 11);
}
assert.equal(bases[0].name, 'stone'); assert.equal(bases[0].height, 9);
assert.equal(bases[1].height, 0); assert.equal(bases[1].name, 'air');
for (let index = 2; index < 256; index++) assert.deepEqual(bases[index], terrain[index]);
assert.deepEqual(chunk.spikes.segments.map(({index, height}) => ({index, height})),
[{index: 0, height: 11}, {index: 0, height: 10}, {index: 1, height: 11}, {index: 1, height: 10}]);
});
test('spike caps commit complete columns and retain original columns when limits would be exceeded', async t => {
await t.test('sixteen per column, oversized runs omitted', async t => {
const app = fixture(t, {minY: 0, height: 20});
app.bot.registry.blocksByStateId[600] = {name: 'pointed_dripstone'};
app.columns.set('-1,-2', app.column((x, y, z) => !z && x < 2 ? (y >= (x ? 3 : 4) ? 600 : 303) : 7));
const {chunks: [chunk]} = await app.snapshot();
const top = decoded(chunk), bases = decoded(chunk, chunk.spikes);
assert.equal(chunk.spikes.segments.length, 16);
assert.ok(chunk.spikes.segments.every(segment => segment.index === 0));
assert.equal(bases[0].height, 4); assert.equal(bases[0].name, 'stone');
assert.deepEqual(bases[1], top[1]);
});
await t.test('chunk budget does not commit partial columns and later smaller runs can fit', async t => {
const app = fixture(t, {minY: 0, height: 20});
app.bot.registry.blocksByStateId[600] = {name: 'pointed_dripstone'};
app.columns.set('-1,-2', app.column((x, y, z) => {
const index = z * 16 + x;
const length = index < 31 ? 16 : index === 31 ? 15 : index === 32 ? 2 : 1;
return y >= 20 - length ? 600 : 303;
}));
const {chunks: [chunk]} = await app.snapshot();
const top = decoded(chunk), bases = decoded(chunk, chunk.spikes);
assert.equal(chunk.spikes.segments.length, 512);
assert.equal(chunk.spikes.segments.filter(segment => segment.index === 31).length, 15);
assert.equal(chunk.spikes.segments.filter(segment => segment.index === 32).length, 0);
assert.equal(chunk.spikes.segments.filter(segment => segment.index === 33).length, 1);
assert.equal(bases[33].height, 19); assert.equal(bases[33].name, 'stone');
for (const index of [32, ...Array.from({length: 222}, (_, offset) => offset + 34)])
assert.deepEqual(bases[index], top[index]);
assert.ok(app.reads <= 256 + 512 + 256 + 3, `bounded spike scan: ${app.reads}`);
});
await t.test('no successfully sampled spikes omits the feature', async t => {
const app = fixture(t, {minY: 0, height: 20});
app.bot.registry.blocksByStateId[600] = {name: 'pointed_dripstone'};
app.columns.set('-1,-2', app.column(() => 600));
const {chunks: [chunk]} = await app.snapshot();
assert.equal(Object.hasOwn(chunk, 'spikes'), false);
assert.ok(decoded(chunk).every(cell => cell.name === 'pointed_dripstone' && cell.height === 20));
assert.equal(app.reads, 256 * 17);
});
await t.test('underwater oversized runs retain pointed terrain and fluid surfaces', async t => {
const app = fixture(t, {minY: 0, height: 20});
app.bot.registry.blocksByStateId[600] = {name: 'pointed_dripstone'};
app.columns.set('-1,-2', app.column((x, y, z) => {
if (z || x > 1) return 7;
if (y === 19) return 202;
return y >= (x ? 2 : 17) ? 600 : 303;
}));
const {chunks: [chunk]} = await app.snapshot();
const top = decoded(chunk), terrain = decoded(chunk, chunk.terrain), fluids = decoded(chunk, chunk.fluids);
const bases = decoded(chunk, chunk.spikes);
assert.equal(chunk.spikes.segments.length, 2);
assert.ok(chunk.spikes.segments.every(segment => segment.index === 0));
assert.equal(bases[0].name, 'stone'); assert.equal(bases[0].height, 17);
assert.deepEqual(bases[1], terrain[1]);
assert.equal(terrain[1].name, 'pointed_dripstone'); assert.equal(terrain[1].height, 19);
assert.equal(top[1].name, 'water'); assert.equal(top[1].height, 20);
assert.deepEqual(fluids[1], {...top[1], level: 0});
});
});
test('spike scans reject unknown states and preserve yield/cancellation bounds without caching partial features', async t => {
await t.test('unknown segment or foundation', async t => {
const app = fixture(t);
app.bot.registry.blocksByStateId[600] = {name: 'pointed_dripstone'};
let unknown = true;
app.columns.set('-1,-2', app.column((_x, y) => y >= 9 ? 600 : unknown ? 999_999 : 303));
await assert.rejects(app.snapshot(), code('INTERNAL_ERROR'));
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
unknown = false;
const {chunks: [chunk]} = await app.snapshot();
assert.equal(chunk.spikes.segments.length, 510);
assert.equal(decoded(chunk, chunk.spikes)[0].height, 13);
});
await t.test('cancel at row yield', async t => {
const app = fixture(t, {minY: 0, height: 20});
app.bot.registry.blocksByStateId[600] = {name: 'pointed_dripstone'};
app.columns.set('-1,-2', app.column((_x, y) => y >= 4 ? 600 : 303));
let stale = false;
setImmediate(() => { stale = true; });
await assert.rejects(app.snapshot({}, () => {
if (stale) throw Object.assign(new Error(), {code: 'SESSION_STALE'});
}), code('SESSION_STALE'));
assert.equal(app.reads, 32 * 17);
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
app.columns.set('-1,-2', app.column(() => 303));
const {chunks: [chunk]} = await app.snapshot();
assert.equal(Object.hasOwn(chunk, 'spikes'), false);
});
});
test('real installed WorldSync and chunk registry produce the same surface wire format', async t => {
const installed = createRequire(require.resolve('@wp2508/mineflayer'));
const registry = installed('@wp2508/minecraft-data')('1.21.4');
const Chunk = installed('prismarine-chunk')('1.21.4');
const World = installed('prismarine-world')('1.21.4');
const chunk = new Chunk({minY: -64, worldHeight: 384});
chunk.setBlockStateId({x: 0, y: 67, z: 0}, registry.blocksByName.grass_block.minStateId);
chunk.setBlockStateId({x: 1, y: 70, z: 0}, registry.blocksByName.water.minStateId);
for (const [x, name, level] of [[3, 'water', 3], [4, 'water', 8], [5, 'water', 15],
[6, 'lava', 0], [7, 'lava', 8], [8, 'lava', 15]]) {
chunk.setBlockStateId({x, y: 70, z: 0}, registry.blocksByName[name].minStateId + level);
chunk.setBlockStateId({x, y: 69, z: 0}, registry.blocksByName.stone.minStateId);
}
const dripstone = registry.blocksByName.pointed_dripstone;
const spike_state = properties => dripstone.minStateId + dripstone.states.reduce((offset, property) =>
offset * property.num_values + (property.values ? property.values.indexOf(properties[property.name])
: properties[property.name] === 'false' ? 1 : 0), 0);
const real_spikes = [];
for (const direction of ['up', 'down']) for (const thickness of ['tip', 'tip_merge', 'frustum', 'middle', 'base']) {
const index = 16 + real_spikes.length;
const state = spike_state({vertical_direction: direction, thickness, waterlogged: 'false'});
chunk.setBlockStateId({x: index % 16, y: 70, z: 1}, state);
chunk.setBlockStateId({x: index % 16, y: 69, z: 1}, registry.blocksByName.stone.minStateId);
real_spikes.push({index, height: 135, direction, thickness});
}
const app = fixture(t, {minY: -64, height: 384});
const world = new World(null, null, {savingInterval: 0});
app.bot.world = world.sync;
app.bot.registry = registry;
app.bot.world.setColumn(-1, -2, chunk, false);
const result = await app.snapshot();
const cells = decoded(result.chunks[0]);
assert.equal(cells[0].height, 132); assert.equal(cells[0].name, 'grass_block');
assert.equal(cells[1].height, 135); assert.equal(cells[1].name, 'water');
assert.equal(cells[2].height, 0);
const fluids = decoded(result.chunks[0], result.chunks[0].fluids);
const terrain = decoded(result.chunks[0], result.chunks[0].terrain);
assert.equal(fluids[1].level, 0); assert.equal(terrain[1].height, 0);
for (const [x, name, level] of [[3, 'water', 3], [4, 'water', 8], [5, 'water', 15],
[6, 'lava', 0], [7, 'lava', 8], [8, 'lava', 15]]) {
assert.equal(fluids[x].name, name); assert.equal(fluids[x].level, level); assert.equal(fluids[x].height, 135);
assert.equal(terrain[x].name, 'stone'); assert.equal(terrain[x].height, 134);
}
assert.deepEqual(result.chunks[0].spikes.segments, real_spikes);
const bases = decoded(result.chunks[0], result.chunks[0].spikes);
for (const {index} of real_spikes) {
assert.equal(terrain[index].name, 'pointed_dripstone'); assert.equal(terrain[index].height, 135);
assert.equal(bases[index].name, 'stone'); assert.equal(bases[index].height, 134);
}
});
function transport_fixture(t) {
const app = fixture(t);
app.columns.set('-1,-2', app.column());
const command = {forward_id: 0, forward_session_id: randomUUID(), node_session_id: 'node-session', node_id: 1, bot_id: 1,
url: 'http://127.0.0.1:1234', path: '/api/forward_server', namespace: '/', token: randomUUID(),
expires_at: Date.now() + 30_000, deadline_at: Date.now() + 2000};
let socket;
const manager = new NodeForwardManager(1, 'node-session', {
stream_factory: () => app.stream,
socket_factory() {
socket = new EventEmitter(); socket.auth = {};
socket.connect = () => { socket.connected = true; queueMicrotask(() => socket.emit('connect')); };
socket.disconnect = () => { socket.connected = false; socket.emit('disconnect'); };
socket.io = {engine: {writeBuffer: []}};
socket.volatile = {emit() {}};
socket.timeout = () => ({emit(event, _payload, ack) {
const {forward_id, forward_session_id, node_session_id, node_id, bot_id, expires_at} = command;
queueMicrotask(() => ack(null, {status: 'success', data: event === 'forward_node_ready' ? {node_ready: true}
: {forward_id, forward_session_id, node_session_id, node_id, bot_id, role: 'node', node_ready: false, expires_at}}));
}});
return socket;
},
});
t.after(() => manager.stop());
return {...app, manager, command, get socket() { return socket; },
rpc(method, payload) { return new Promise(resolve => socket.emit('relay_request', request(method, payload, command.forward_session_id), resolve)); },
};
}
test('manager dispatches world reads through guarded RPC and rejects malformed requests and disconnect during yield', async t => {
const app = transport_fixture(t);
assert.equal((await app.manager.ensure(app.command)).status, 'success');
const snapshot = await app.rpc('world.snapshot', {radius: 1, known: []});
assert.equal(snapshot.status, 'success'); assert.match(snapshot.data.watch_id, uuid);
assert.deepEqual(await app.rpc('world.unwatch', {watch_id: snapshot.data.watch_id}), {status: 'success', data: {released: true}});
assert.equal((await app.rpc('world.snapshot', {radius: 4, known: []})).data.code, 'INVALID_REQUEST');
const pending = app.rpc('world.snapshot', {radius: 1, known: []});
setImmediate(() => app.socket.disconnect());
assert.equal((await pending).data.code, 'SESSION_STALE');
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
});
+293
View File
@@ -0,0 +1,293 @@
import assert from 'node:assert/strict';
import {randomUUID} from 'node:crypto';
import {EventEmitter} from 'node:events';
import {createRequire, isBuiltin} from 'node:module';
import {fileURLToPath} from 'node:url';
import test from 'node:test';
import {build} from 'esbuild';
const require = createRequire(import.meta.url);
const installed = createRequire(require.resolve('@wp2508/mineflayer'));
const registry = installed('@wp2508/minecraft-data')('1.21.4');
const compiled = await build({
stdin: {contents: `export * from './bot/forward/stream.ts'; export * from './utils/forward_protocol.ts';`,
resolveDir: fileURLToPath(new URL('../', import.meta.url)), loader: 'ts'},
bundle: true, platform: 'node', format: 'cjs', write: false,
plugins: [{name: 'isolated-world', setup(build) {
build.onResolve({filter: /(?:instance\/index\.ts|console_print\.ts|path_utils\.ts)$/}, args =>
({path: args.path.includes('instance/') ? 'registry' : args.path.endsWith('path_utils.ts') ? 'path' : 'console', namespace: 'stub'}));
build.onLoad({filter: /.*/, namespace: 'stub'}, ({path}) => ({loader: 'js', contents: {
registry: 'export const get_bot_context = () => undefined; export const observe_bot_context = () => () => {};',
console: 'export const console_print = {success: x => x, log() {}};',
path: 'export const get_project_root_path = () => {throw Error("Unexpected filesystem access")};',
}[path]}));
}}],
});
const module = {exports: {}};
new Function('require', 'module', 'exports', compiled.outputFiles[0].text)(specifier => {
assert.ok(isBuiltin(specifier));
return require(specifier);
}, module, module.exports);
const {BotStream, validate_rpc} = module.exports;
const code = expected => error => error.code === expected;
const state = (name, properties = {}) => {
const block = registry.blocksByName[name];
return block.minStateId + block.states.reduce((offset, property) => {
const value = properties[property.name];
const index = property.type === 'bool' ? value === true ? 0 : 1 : property.values.indexOf(String(value));
assert.ok(index >= 0, `${name}.${property.name}`);
return offset * property.num_values + index;
}, 0);
};
function fixture(t, {minY = -4, height = 32} = {}) {
let now = 100, reads = 0;
const columns = new Map();
const bot = Object.assign(new EventEmitter(), {
chat() {}, version: '1.21.4', game: {minY, height, dimension: 'overworld'}, registry,
world: {getColumn(x, z) {return columns.get(`${x},${z}`);}},
entity: {position: {x: -0.25, y: 4, z: -16.25}, yaw: 0},
});
const context = Object.assign(new EventEmitter(), {bot_status: 'online', bot_instance: bot});
let current = context;
const stream = new BotStream(1, {source: {get: () => current, observe: () => () => {}}, now: () => now});
t.after(() => stream.dispose());
const column = (get = () => registry.blocksByName.stone.defaultState) => ({getBlockStateId(p) {
assert.ok(p.x >= 0 && p.x < 16 && p.z >= 0 && p.z < 16);
assert.ok(p.y >= bot.game.minY && p.y < bot.game.minY + bot.game.height);
reads++;
return get(p.x, p.y, p.z);
}});
columns.set('-1,-2', column());
return {bot, context, stream, columns, column,
get reads() {return reads;},
time(value) {now = value;}, set_context(value) {current = value;},
snapshot(input = {}) {return stream.world_snapshot({radius: 1, known: [], ...input}, now + 2000);},
chunk(snapshot, input = {}, check = () => {}) {
return stream.world_chunk({x: -1, z: -2, world_generation: snapshot.world_generation,
watch_id: snapshot.watch_id, ...input}, now + 2000, check);
},
};
}
function decode(chunk) {
assert.deepEqual(Object.keys(chunk).sort(), ['x', 'z', 'revision', 'world_generation', 'min_y', 'height', 'palette', 'sections'].sort());
assert.ok(chunk.palette.length >= 1 && chunk.palette.length <= 8192);
let previous = -1;
const sections = new Map();
for (const section of chunk.sections) {
assert.ok(section.y > previous && section.y < Math.ceil(chunk.height / 16));
previous = section.y;
const bytes = Buffer.from(section.blocks, 'base64');
assert.equal(bytes.length, 8192);
assert.equal(bytes.toString('base64'), section.blocks);
for (let i = 0; i < 4096; i++) assert.ok(bytes.readUInt16LE(i * 2) < chunk.palette.length);
sections.set(section.y, bytes);
}
return (x, y, z) => {
const offset = y - chunk.min_y, bytes = sections.get(Math.floor(offset / 16));
return bytes ? chunk.palette[bytes.readUInt16LE(((offset % 16) * 256 + z * 16 + x) * 2)] : {name: 'air'};
};
}
test('world.chunk validates exact integer coordinates and UUIDs without invoking accessors', () => {
const payload = {x: -1, z: 2, world_generation: randomUUID(), watch_id: randomUUID()};
const request = value => ({method: 'world.chunk', payload: value, request_id: randomUUID(), correlation_id: randomUUID(),
forward_session_id: randomUUID(), deadline_at: 1000});
assert.equal(validate_rpc(request(payload)).payload, payload);
for (const invalid of [{}, {...payload, x: 1.5}, {...payload, z: Infinity}, {...payload, x: '1'},
{...payload, watch_id: undefined}, {...payload, world_generation: 'bad'}, {...payload, radius: 3}])
assert.throws(() => validate_rpc(request(invalid)), code('INVALID_REQUEST'));
for (const key of Object.keys(payload)) {
let reads = 0;
const invalid = Object.defineProperty({...payload}, key, {enumerable: true, get() {reads++; return payload[key];}});
assert.throws(() => validate_rpc(request(invalid)), code('INVALID_REQUEST'));
assert.equal(reads, 0);
}
});
test('real installed chunks retain cave floors, interiors, roofs and light below the surface', async t => {
const app = fixture(t, {minY: -64, height: 384});
const Chunk = installed('prismarine-chunk')('1.21.4');
const World = installed('prismarine-world')('1.21.4');
const column = new Chunk({minY: -64, worldHeight: 384});
const world = new World(null, null, {savingInterval: 0});
app.bot.world = world.sync;
app.bot.world.setColumn(-1, -2, column, false);
for (const [x, y, z, name] of [[0, -64, 0, 'bedrock'], [0, -40, 0, 'diamond_ore'], [0, 80, 0, 'stone'],
[15, 79, 15, 'glass'], [1, -39, 2, 'cave_air'], [1, -38, 2, 'void_air']])
column.setBlockStateId({x, y, z}, registry.blocksByName[name].defaultState);
column.setBlockStateId({x: 2, y: -20, z: 3}, state('light', {level: 9, waterlogged: false}));
const snapshot = await app.snapshot(), chunk = await app.chunk(snapshot), cell = decode(chunk);
assert.equal(chunk.revision, snapshot.loaded[0].revision);
assert.deepEqual(chunk.sections.map(s => s.y), [0, 1, 2, 8, 9]);
assert.equal(cell(0, -64, 0).name, 'bedrock');
assert.equal(cell(0, -40, 0).name, 'diamond_ore');
assert.equal(cell(0, -39, 0).name, 'air');
assert.equal(cell(0, 80, 0).name, 'stone');
assert.equal(cell(15, 79, 15).name, 'glass');
assert.equal(cell(1, -39, 2).name, 'cave_air');
assert.equal(cell(1, -38, 2).name, 'void_air');
assert.deepEqual(cell(2, -20, 3).properties, {waterlogged: false, level: 9});
const unchanged = await app.snapshot({world_generation: snapshot.world_generation, known: snapshot.loaded, watch_id: snapshot.watch_id});
assert.deepEqual(unchanged.chunks, []);
});
test('palette preserves generic typed registry properties, deduplicates state IDs and excludes partial occluders', async t => {
const app = fixture(t);
const samples = [
['wheat', {age: 7}], ['pink_petals', {facing: 'south', flower_amount: 4}],
['snow', {layers: 6}], ['pointed_dripstone', {thickness: 'frustum', vertical_direction: 'down', waterlogged: true}],
['lever', {face: 'ceiling', facing: 'east', powered: true}], ['cake', {bites: 5}],
['beehive', {facing: 'west', honey_level: 4}], ['oak_slab', {type: 'bottom', waterlogged: false}],
['oak_leaves', {distance: 3, persistent: true, waterlogged: false}], ['glass', {}], ['torch', {}],
['water', {level: 8}], ['stone', {}], ['oak_slab', {type: 'top', waterlogged: false}],
];
const states = samples.map(([name, properties]) => state(name, properties));
app.columns.set('-1,-2', app.column((x, y, z) => y === -3 && z === 3 && x < states.length ? states[x]
: y === 27 && z === 3 && x === 0 ? states[0] : registry.blocksByName.air.defaultState));
const chunk = await app.chunk(await app.snapshot()), cell = decode(chunk);
for (let x = 0; x < samples.length; x++) {
const [name, properties] = samples[x], result = cell(x, -3, 3);
assert.equal(result.name, name);
assert.deepEqual(result.properties, properties);
assert.equal(typeof result.color, 'number');
assert.equal(result.occluding, name === 'stone' || name === 'beehive');
}
assert.equal(chunk.palette.filter(p => p.name === 'wheat').length, 1);
assert.equal(chunk.palette.filter(p => p.name === 'oak_slab').length, 2);
assert.equal(cell(0, 27, 3).properties.age, 7);
});
test('sections use offsets from nonaligned minimum, pad with air and omit only air variants', async t => {
const app = fixture(t, {minY: -3, height: 33});
app.columns.set('-1,-2', app.column((x, y, z) => y === 29 && x === 15 && z === 14 ? state('light', {level: 0, waterlogged: true})
: [registry.blocksByName.air.defaultState, registry.blocksByName.cave_air.defaultState, registry.blocksByName.void_air.defaultState][(x + z) % 3]));
const chunk = await app.chunk(await app.snapshot());
assert.deepEqual(chunk.sections.map(s => s.y), [2]);
assert.equal(decode(chunk)(15, 29, 14).name, 'light');
const last = Buffer.from(chunk.sections[0].blocks, 'base64');
for (let i = 256; i < 4096; i++) assert.equal(chunk.palette[last.readUInt16LE(i * 2)].name, 'air');
app.bot.emit('blockUpdate', null, {position: {x: -1, z: -17}});
app.columns.set('-1,-2', app.column(() => registry.blocksByName.cave_air.defaultState));
const empty = await app.chunk(await app.snapshot());
assert.equal(empty.palette.length, 1);
assert.deepEqual(empty.sections, []);
});
test('chunk requires a live watch in its generation and radius, and never renews its lease', async t => {
const app = fixture(t), snapshot = await app.snapshot();
await assert.rejects(app.chunk(snapshot, {watch_id: randomUUID()}), code('CANCELLED'));
await assert.rejects(app.chunk(snapshot, {world_generation: randomUUID()}), code('CANCELLED'));
const before = app.reads;
await assert.rejects(app.chunk(snapshot, {x: 1}), code('FORBIDDEN'));
assert.equal(app.reads, before);
app.time(44_000);
await app.chunk(snapshot);
app.time(45_101);
await assert.rejects(app.chunk(snapshot), code('CANCELLED'));
assert.equal(app.bot.listenerCount('blockUpdate'), 0);
const fresh = await app.snapshot();
app.bot.game.dimension = 'the_nether';
await assert.rejects(app.chunk(fresh), code('CANCELLED'));
});
test('voxel reads cancel after at most 2048 reads on deadline, transport and changing loaded entry', async t => {
for (const kind of ['deadline', 'transport', 'unload', 'replace', 'block', 'respawn', 'move', 'context', 'offline', 'unwatch']) {
await t.test(kind, async t => {
const app = fixture(t, {height: 64}), snapshot = await app.snapshot();
const before = app.reads;
let disconnected = false;
setImmediate(() => {
if (kind === 'deadline') app.time(5000);
if (kind === 'transport') disconnected = true;
if (kind === 'unload') app.columns.delete('-1,-2');
if (kind === 'replace') app.columns.set('-1,-2', app.column());
if (kind === 'block') app.bot.emit('blockUpdate', null, {position: {x: -1, z: -17}});
if (kind === 'respawn') app.bot.emit('respawn');
if (kind === 'move') app.bot.entity.position.x = 32;
if (kind === 'context') app.set_context(undefined);
if (kind === 'offline') app.context.bot_status = 'offline';
if (kind === 'unwatch') app.stream.world_unwatch({watch_id: snapshot.watch_id}, 2100);
});
await assert.rejects(app.chunk(snapshot, {}, () => {
if (disconnected) throw Object.assign(new Error(), {code: 'SESSION_STALE'});
}), code(kind === 'deadline' ? 'TIMEOUT' : kind === 'transport' ? 'SESSION_STALE' : kind === 'offline' ? 'BOT_NOT_ONLINE' : 'CANCELLED'));
assert.equal(app.reads - before, 2048);
});
}
});
test('two voxel requests may run independently of four surface requests', async t => {
const app = fixture(t, {height: 64}), snapshot = await app.snapshot();
const pending = [app.chunk(snapshot), app.chunk(snapshot)];
await assert.rejects(app.chunk(snapshot), code('RATE_LIMITED'));
assert.equal((await Promise.all(pending)).length, 2);
await app.chunk(snapshot);
});
test('cache shares surface revisions, returns detached data and invalidates on block events, unload and idle', async t => {
const app = fixture(t), snapshot = await app.snapshot();
const first = await app.chunk(snapshot), before = app.reads;
first.palette[0].name = 'mutated'; first.sections.length = 0;
const second = await app.chunk(snapshot);
assert.equal(app.reads, before);
assert.equal(second.palette[0].name, 'stone'); assert.equal(second.sections.length, 2);
app.bot.emit('blockUpdate', null, {position: {x: -1, z: -17}});
const third = await app.chunk(snapshot);
assert.notEqual(third.revision, second.revision);
const updated = await app.snapshot({world_generation: snapshot.world_generation, known: snapshot.loaded, watch_id: snapshot.watch_id});
assert.equal(updated.loaded[0].revision, third.revision);
app.columns.delete('-1,-2');
await assert.rejects(app.chunk(updated), code('CANCELLED'));
app.columns.set('-1,-2', app.column());
const reloaded = await app.chunk(updated);
assert.notEqual(reloaded.revision, third.revision);
app.stream.world_unwatch({watch_id: snapshot.watch_id}, 2100);
await assert.rejects(app.chunk(snapshot), code('CANCELLED'));
});
test('voxel LRU retains at most eight columns', async t => {
const app = fixture(t, {height: 1});
for (let z = -3; z <= -1; z++) for (let x = -2; x <= 0; x++) app.columns.set(`${x},${z}`, app.column());
const snapshot = await app.snapshot();
const coordinates = snapshot.loaded;
for (const {x, z} of coordinates) await app.chunk(snapshot, {x, z});
const before_cached = app.reads;
await app.chunk(snapshot, {x: coordinates[8].x, z: coordinates[8].z});
assert.equal(app.reads, before_cached);
await app.chunk(snapshot, {x: coordinates[0].x, z: coordinates[0].z});
assert.equal(app.reads - before_cached, 256);
});
test('unknown interior states and excessive palettes fail without caching partial data', async t => {
const app = fixture(t, {minY: 0, height: 64});
let unknown = true;
app.columns.set('-1,-2', app.column((_x, y) => y === 0 && unknown ? 999_999 : registry.blocksByName.stone.defaultState));
const snapshot = await app.snapshot();
await assert.rejects(app.chunk(snapshot), code('INTERNAL_ERROR'));
unknown = false;
assert.equal((await app.chunk(snapshot)).sections.length, 4);
app.bot.registry = {blocksByStateId: Object.fromEntries(Array.from({length: 8193}, (_, i) => [i, {name: `block_${i}`}]))};
app.columns.set('-1,-2', app.column((x, y, z) => Math.min(8192, y * 256 + z * 16 + x)));
await assert.rejects(app.chunk(snapshot), code('INTERNAL_ERROR'));
app.bot.registry.blocksByStateId[8192] = app.bot.registry.blocksByStateId[0];
app.columns.set('-1,-2', app.column((x, y, z) => (y * 256 + z * 16 + x) % 8192));
assert.equal((await app.chunk(snapshot)).palette.length, 8192);
});
test('invalid generic registry properties are rejected and maximum height reads every cell', async t => {
const app = fixture(t, {minY: -4096, height: 4096}), snapshot = await app.snapshot();
const before = app.reads, chunk = await app.chunk(snapshot);
assert.equal(app.reads - before, 4096 * 256);
assert.equal(chunk.sections.length, 256);
assert.equal(chunk.sections[255].y, 255);
assert.equal(decode(chunk)(15, -1, 15).name, 'stone');
for (const states of [Array.from({length: 33}, (_, i) => ({name: `p${i}`, type: 'bool', num_values: 2})),
[{name: 'age', type: 'int', num_values: 1, values: ['NaN']}],
[{name: 'shape', type: 'enum', num_values: 1, values: ['x'.repeat(65)]}]]) {
app.bot.registry = {blocksByStateId: {0: {name: 'bad', minStateId: 0, states}}};
app.columns.set('-1,-2', app.column(() => 0));
await assert.rejects(app.chunk(snapshot), code('INTERNAL_ERROR'));
}
});