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
+79 -3
View File
@@ -12,11 +12,13 @@ import type {RunSnapshot, StartResult, WorkflowManager, WorkflowCommand, Workflo
import type {WorkflowCatalog} from '../../types/bot/workflow/metadata.ts';
import {get_workflow_catalog} from '../workflow/catalog.ts';
import {summarize_run, type RunSummary} from '../workflow/runtime/summary.ts';
import {object, validate_workflow_payload} from '../../utils/forward_protocol.ts';
import {object, validate_workflow_payload, validate_world_payload} from '../../utils/forward_protocol.ts';
import {WorldPreview, type WorldBot} from './world.ts';
import type {WorldSnapshot, WorldVoxelChunk} from '../../types/forward.ts';
export interface StreamContext {
readonly bot_status: bot_status;
readonly bot_instance: {
readonly bot_instance: ({
chat(message: string): unknown;
players?: Record<string, unknown>;
tabComplete?(text: string, assumeCommand?: boolean, sendBlockInSight?: boolean, timeout?: number): Promise<unknown[]>;
@@ -28,7 +30,7 @@ export interface StreamContext {
on(name: string, listener: (packet: any) => void): unknown;
off(name: string, listener: (packet: any) => void): unknown;
};
} | null;
} & Partial<Omit<WorldBot, 'game'>> & {game?: Partial<WorldBot['game']>}) | null;
readonly logger?: {read_recent_log(count: number): Promise<LogHistoryRecord[]>};
readonly info_manager?: Pick<InfoManager, 'get_all_info'> | null;
readonly function_manager?: Pick<FunctionManager, 'run'> | null;
@@ -326,6 +328,7 @@ export class BotStream {
private context_listeners: Array<[string, (...args: any[]) => void]> = [];
private unobserve: (() => void) | undefined;
private disposed = false;
private world_preview?: WorldPreview;
constructor(readonly bot_id: number, options: BotStreamOptions = {}) {
if (!Number.isSafeInteger(bot_id) || bot_id < 1) throw new BotStreamError('INVALID_REQUEST', 'Invalid bot ID');
@@ -402,6 +405,8 @@ export class BotStream {
}
private detach() {
this.world_preview?.dispose();
this.world_preview = undefined;
for (const [event, listener] of this.context_listeners) this.context?.off(event, listener);
this.context_listeners = [];
}
@@ -414,6 +419,10 @@ export class BotStream {
}
private status_event() {
if (this.context?.bot_status !== 'online' || this.world_preview?.bot !== this.context?.bot_instance) {
this.world_preview?.dispose();
this.world_preview = undefined;
}
this.publish('status_update', this.state());
}
@@ -463,6 +472,73 @@ export class BotStream {
};
}
private world_context(deadline_at: number, check_transport: () => void, existing = false) {
this.active();
check_transport();
if (!Number.isSafeInteger(deadline_at) || deadline_at <= this.now())
throw new BotStreamError('TIMEOUT', 'World request deadline expired');
const context = this.context;
if (!context) throw new BotStreamError('NO_CONTEXT', 'Bot context does not exist');
const instance = context.bot_instance;
if (context.bot_status !== 'online' || !instance) {
this.world_preview?.dispose();
this.world_preview = undefined;
throw new BotStreamError('BOT_NOT_ONLINE', 'Bot is not online');
}
if (!instance.world || !instance.game || !instance.entity || !instance.registry || !instance.version
|| typeof instance.on !== 'function' || typeof instance.off !== 'function')
throw new BotStreamError('INTERNAL_ERROR', 'Bot world is unavailable');
if (this.world_preview?.bot !== instance || this.world_preview?.is_disposed) {
this.world_preview?.dispose();
this.world_preview = undefined;
}
if (existing && !this.world_preview) throw new BotStreamError('CANCELLED', 'World watch is unavailable');
const service = this.world_preview ??= new WorldPreview(instance as WorldBot, generation => {
if (!this.disposed && this.source.get(this.bot_id) === context && context.bot_instance === instance
&& context.bot_status === 'online') this.publish('info_update', {name: 'world_preview', value: {world_generation: generation}});
}, this.now);
const check = () => {
check_transport();
if (deadline_at <= this.now()) throw new BotStreamError('TIMEOUT', 'World request deadline expired');
if (this.disposed || this.source.get(this.bot_id) !== context || context.bot_instance !== instance
|| this.world_preview !== service) {
service.dispose();
throw new BotStreamError('CANCELLED', 'Bot context changed');
}
if (context.bot_status !== 'online') {
service.dispose();
throw new BotStreamError('BOT_NOT_ONLINE', 'Bot is not online');
}
};
check();
return {service, check};
}
async world_snapshot(payload: unknown, deadline_at: number, check_transport: () => void = () => {}): Promise<WorldSnapshot> {
const input = structuredClone(validate_world_payload('world.snapshot', payload));
const {service, check} = this.world_context(deadline_at, check_transport);
const result = await service.snapshot(input, check);
check();
return {...result, context_generation: this.generation!, stream_id: this.stream_id};
}
async world_chunk(payload: unknown, deadline_at: number, check_transport: () => void = () => {}): Promise<WorldVoxelChunk> {
const input = structuredClone(validate_world_payload('world.chunk', payload));
const {service, check} = this.world_context(deadline_at, check_transport, true);
const result = await service.chunk(input, check);
check();
return result;
}
world_unwatch(payload: unknown, deadline_at: number, check_transport: () => void = () => {}): {released: true} {
const input = validate_world_payload('world.unwatch', payload);
this.active();
check_transport();
if (!Number.isSafeInteger(deadline_at) || deadline_at <= this.now())
throw new BotStreamError('TIMEOUT', 'World request deadline expired');
return this.world_preview?.unwatch(input.watch_id) ?? {released: true};
}
get_context_info(payload: unknown, deadline_at: number): InfoSnapshot[] {
this.active();
if (!exact(payload, [])) throw new BotStreamError('INVALID_REQUEST', 'Invalid info request');