Skip to content

src/lib/agent.ts

75 lines · 2.3 KB · TypeScript
Raw
import { LIMITS } from '../shared/world-rules.ts'
import { agentPlayer, getCaller } from './identity.ts'
import {
	WorldError,
	applyBlockOps,
	ownedBuild,
	parseBlockOp,
	requireUuid,
	sleep,
	type BlockOp,
} from './world-ops.ts'

export async function requireAgent(agentName: unknown) {
	const caller = await getCaller()
	if (!caller) throw new WorldError('Sign in to Kody to build.', 401)
	return { caller, player: agentPlayer(caller, agentName) }
}

/**
 * Stream block writes for a build in small batches so every viewer sees the
 * structure appear block-by-block.
 */
export async function streamBuildOps(input: {
	buildId: unknown
	blocks: unknown
	kind: 'put' | 'del'
	batchSize?: unknown
	delayMs?: unknown
}) {
	const caller = await getCaller()
	if (!caller) throw new WorldError('Sign in to Kody to build.', 401)
	const buildId = requireUuid(input.buildId, 'buildId')
	const build = await ownedBuild(buildId, caller.username)
	if (build.status === 'done' || build.status === 'failed') {
		throw new WorldError('That build is finished. Start a new build.', 409)
	}
	if (!Array.isArray(input.blocks)) throw new WorldError('blocks must be an array of {x, y, z, material}.')
	if (input.blocks.length > LIMITS.maxAgentBlocksPerCall) {
		throw new WorldError(
			`At most ${LIMITS.maxAgentBlocksPerCall} blocks per call. Split the build into several calls.`,
		)
	}
	const ops: Array<BlockOp> = []
	let rejected = 0
	for (const raw of input.blocks) {
		const op = parseBlockOp(raw, input.kind)
		if (op) ops.push(op)
		else rejected++
	}
	const batchSize = clampInt(input.batchSize, 1, 60, 24)
	const delayMs = clampInt(input.delayMs, 0, 1000, 150)
	let placed = 0
	let removed = 0
	let batches = 0
	for (let i = 0; i < ops.length; i += batchSize) {
		const chunk = ops.slice(i, i + batchSize)
		const out = await applyBlockOps({
			player: build.player,
			worldId: build.worldId,
			ops: chunk,
			buildId: input.kind === 'put' ? buildId : null,
		})
		placed += out.placed
		removed += out.removed
		batches++
		if (delayMs && i + batchSize < ops.length) await sleep(delayMs)
	}
	return { buildId, placed, removed, rejected, batches }
}

function clampInt(value: unknown, min: number, max: number, fallback: number) {
	const n = Number(value)
	if (!Number.isFinite(n)) return fallback
	return Math.max(min, Math.min(max, Math.round(n)))
}