From 39a5acebfd2a83aa3cee9e11c798545aaf9e0fad Mon Sep 17 00:00:00 2001 From: Rin Date: Sat, 10 Jan 2026 17:26:40 +0800 Subject: [PATCH] feat(minecraft): implement raw event pipeline --- .../perception/attention-detector.ts | 229 +++++++++++++++--- .../src/cognitive/perception/event-manager.ts | 1 - .../src/cognitive/perception/pipeline.ts | 20 +- .../minecraft/src/libs/mineflayer/core.ts | 4 + 4 files changed, 219 insertions(+), 35 deletions(-) diff --git a/services/minecraft/src/cognitive/perception/attention-detector.ts b/services/minecraft/src/cognitive/perception/attention-detector.ts index 177bc098b..09a94b325 100644 --- a/services/minecraft/src/cognitive/perception/attention-detector.ts +++ b/services/minecraft/src/cognitive/perception/attention-detector.ts @@ -1,70 +1,235 @@ +import type { Logg } from '@guiiai/logg' + import type { EventManager } from './event-manager' import type { RawPerceptionEvent } from './raw-events' import { LeakyBucket } from './leaky-bucket' -export interface AttentionEventPayload { - raw: RawPerceptionEvent - intensity: number +export interface PlayerAttentionEventPayload { + kind: 'player' + playerName?: string + hasLineOfSight?: boolean + distance?: number + playerAction: 'move' | 'punch' | 'teabag' | 'sound' | 'damage' | 'pickup' } +export interface MobAttentionEventPayload { + kind: 'mob' + mobName?: string + hasLineOfSight?: boolean + distance?: number + mobAction: string +} + +export type AttentionEventPayload = PlayerAttentionEventPayload | MobAttentionEventPayload + export class AttentionDetector { private readonly buckets = new Map() - constructor(private readonly deps: { eventManager: EventManager }) { } + private readonly movementState = new Map< + string, + { + movingSince: number + lastSeenMove: number + emitted: boolean + } + >() + + constructor( + private readonly deps: { + eventManager: EventManager + logger: Logg + }, + ) { } public tick(deltaMs: number): void { for (const bucket of this.buckets.values()) { bucket.tick(deltaMs) } + + const now = Date.now() + for (const [id, state] of this.movementState.entries()) { + if (now - state.lastSeenMove > 250) { + this.movementState.delete(id) + } + } } public ingest(event: RawPerceptionEvent): void { - const key = this.keyOf(event) - const bucket = this.getBucket(key) + const dispatch: Record void>> = { + sighted: { + arm_swing: event => this.onPunch(event as any), + sneak_toggle: event => this.onSneakToggle(event as any), + entity_moved: event => this.onMove(event as any), + }, + heard: { + sound: event => this.onSound(event as any), + }, + felt: { + damage_taken: event => this.onDamage(event as any), + item_collected: event => this.onPickup(event as any), + }, + } + dispatch[event.modality]?.[event.kind]?.(event) + } - const weight = 1 - const { fired, value } = bucket.add(weight) + private onPunch(event: Extract): void { + // Heuristic: 3 swings in ~1s triggers + const bucket = this.getBucket(`punch:${event.entityId}`, { + capacity: 3, + leakPerSecond: 3, + trigger: 3, + }) + const { fired } = bucket.add(1) if (!fired) return + this.emitAttention({ + kind: 'player', + playerName: event.displayName, + hasLineOfSight: event.hasLineOfSight, + distance: event.distance, + playerAction: 'punch', + }) + } + + private onSneakToggle(event: Extract): void { + // >= 4 toggles within 2s (leaky bucket approximation) + const bucket = this.getBucket(`teabag:${event.entityId}`, { + capacity: 4, + leakPerSecond: 2, + trigger: 4, + }) + + const { fired } = bucket.add(1) + if (!fired) + return + + this.emitAttention({ + kind: 'player', + playerName: event.displayName, + hasLineOfSight: event.hasLineOfSight, + distance: event.distance, + playerAction: 'teabag', + }) + } + + private onMove(event: Extract): void { + // Only count players for "attracting attention" + if (event.entityType !== 'player') + return + + const now = Date.now() + const state = this.movementState.get(event.entityId) + if (!state) { + this.movementState.set(event.entityId, { + movingSince: now, + lastSeenMove: now, + emitted: false, + }) + return + } + + state.lastSeenMove = now + if (state.emitted) + return + + if (now - state.movingSince < 600) + return + + // Cooldown gate to avoid spamming: 1 trigger, leaks over ~3s + const bucket = this.getBucket(`move:${event.entityId}`, { + capacity: 1, + leakPerSecond: 1 / 3, + trigger: 1, + }) + + const { fired } = bucket.add(1) + if (!fired) + return + + state.emitted = true + this.emitAttention({ + kind: 'player', + playerName: event.displayName, + hasLineOfSight: event.hasLineOfSight, + distance: event.distance, + playerAction: 'move', + }) + } + + private onSound(event: Extract): void { + // Any sound within range is "interesting". Gate by soundId to prevent spam. + const bucket = this.getBucket(`sound:${event.soundId}`, { + capacity: 1, + leakPerSecond: 1, // ~1s cooldown per soundId + trigger: 1, + }) + + const { fired } = bucket.add(1) + if (!fired) + return + + this.emitAttention({ + kind: 'player', + playerAction: 'sound', + distance: event.distance, + }) + } + + private onDamage(_event: Extract): void { + // Self-damage is intrinsically salient; gate with small cooldown + const bucket = this.getBucket('felt:damage', { + capacity: 1, + leakPerSecond: 1 / 2, + trigger: 1, + }) + + const { fired } = bucket.add(1) + if (!fired) + return + + this.emitAttention({ + kind: 'player', + playerAction: 'damage', + }) + } + + private onPickup(_event: Extract): void { + // Item pickup can be spammy (e.g. farms); apply a small cooldown + const bucket = this.getBucket('felt:pickup', { + capacity: 1, + leakPerSecond: 1, // ~1s cooldown + trigger: 1, + }) + + const { fired } = bucket.add(1) + if (!fired) + return + + this.emitAttention({ + kind: 'player', + playerAction: 'pickup', + }) + } + + private emitAttention(payload: AttentionEventPayload): void { this.deps.eventManager.emit({ type: 'perception', - payload: { - raw: event, - intensity: value, - }, + payload, source: { type: 'minecraft', id: 'perception' }, timestamp: Date.now(), }) } - private getBucket(key: string): LeakyBucket { + private getBucket(key: string, config: { capacity: number, leakPerSecond: number, trigger: number }): LeakyBucket { const existing = this.buckets.get(key) if (existing) return existing - const created = new LeakyBucket({ - capacity: 10, - leakPerSecond: 2, - trigger: 3, - }) - + const created = new LeakyBucket(config) this.buckets.set(key, created) return created } - - private keyOf(event: RawPerceptionEvent): string { - switch (event.modality) { - case 'sighted': - return `sighted:${event.kind}:${(event as any).entityId ?? 'unknown'}` - case 'heard': - return `heard:${event.kind}:${event.soundId}` - case 'felt': - return `felt:${event.kind}` - default: - return 'unknown' - } - } } diff --git a/services/minecraft/src/cognitive/perception/event-manager.ts b/services/minecraft/src/cognitive/perception/event-manager.ts index a963fd0cc..330fb096c 100644 --- a/services/minecraft/src/cognitive/perception/event-manager.ts +++ b/services/minecraft/src/cognitive/perception/event-manager.ts @@ -9,7 +9,6 @@ export class EventManager { // TODO: Temporal Context tracking // TODO: Salience Detection / Filtering noise - // Sort/Filter logic could go here in the future if (!event.priority) { event.priority = 0 // Default priority } diff --git a/services/minecraft/src/cognitive/perception/pipeline.ts b/services/minecraft/src/cognitive/perception/pipeline.ts index 1bbdb4211..a24e85e33 100644 --- a/services/minecraft/src/cognitive/perception/pipeline.ts +++ b/services/minecraft/src/cognitive/perception/pipeline.ts @@ -5,11 +5,13 @@ import type { EventManager } from './event-manager' import type { RawPerceptionEvent } from './raw-events' import { AttentionDetector } from './attention-detector' +import { MineflayerPerceptionCollector } from './mineflayer-perception-collector' import { RawEventBuffer } from './raw-event-buffer' export class PerceptionPipeline { private readonly buffer = new RawEventBuffer() private readonly detector: AttentionDetector + private collector: MineflayerPerceptionCollector | null = null private initialized = false constructor( @@ -18,14 +20,28 @@ export class PerceptionPipeline { logger: Logg }, ) { - this.detector = new AttentionDetector({ eventManager: this.deps.eventManager }) + this.detector = new AttentionDetector({ + eventManager: this.deps.eventManager, + logger: this.deps.logger, + }) } - public init(_bot: MineflayerWithAgents): void { + public init(bot: MineflayerWithAgents): void { this.initialized = true + + this.collector = new MineflayerPerceptionCollector({ + logger: this.deps.logger, + emitRaw: (event) => { + this.collect(event) + }, + maxDistance: 32, + }) + this.collector.init(bot) } public destroy(): void { + this.collector?.destroy() + this.collector = null this.buffer.clear() this.initialized = false } diff --git a/services/minecraft/src/libs/mineflayer/core.ts b/services/minecraft/src/libs/mineflayer/core.ts index ae0af0446..b3994b8ed 100644 --- a/services/minecraft/src/libs/mineflayer/core.ts +++ b/services/minecraft/src/libs/mineflayer/core.ts @@ -200,6 +200,10 @@ export class Mineflayer extends EventEmitter { this.ticker.on(event, cb) } + public offTick(event: TickEvents, cb: TickEventsHandler) { + this.ticker.off(event, cb) + } + public async stop() { for (const plugin of this.options?.plugins || []) { if (plugin.beforeCleanup) {