From c4b35f650af38d9da05f37f1a0324667b6d8d176 Mon Sep 17 00:00:00 2001 From: Rin Date: Thu, 15 Jan 2026 05:19:35 +0800 Subject: [PATCH] feat(minecraft): event on player join, wire up eventbus --- .../src/cognitive/conscious/brain.ts | 57 +++++++------ services/minecraft/src/cognitive/index.ts | 16 +++- .../mineflayer-perception-collector.ts | 82 +++++++++++++++++++ .../cognitive/perception/types/raw-events.ts | 13 ++- .../src/cognitive/perception/types/signals.ts | 1 + .../cognitive/rules/social/player-joined.yaml | 19 +++++ 6 files changed, 158 insertions(+), 30 deletions(-) create mode 100644 services/minecraft/src/cognitive/rules/social/player-joined.yaml diff --git a/services/minecraft/src/cognitive/conscious/brain.ts b/services/minecraft/src/cognitive/conscious/brain.ts index 7711de560..3c38abed7 100644 --- a/services/minecraft/src/cognitive/conscious/brain.ts +++ b/services/minecraft/src/cognitive/conscious/brain.ts @@ -130,30 +130,10 @@ export class Brain { this.debugService = DebugService.getInstance() } - public init(bot: MineflayerWithAgents): void { - this.log('INFO', 'Brain: Initializing...') - this.bot = bot - this.blackboard.update({ selfUsername: bot.username }) - - // Perception Signal Handler - Only process chat messages for now - this.deps.eventManager.on('perception', async (event) => { - const signal = event.payload - // Only handle chat messages in the deliberative layer - if (signal.type !== 'chat_message') - return - - this.log('INFO', `Brain: Received chat: ${signal.description}`) - - // Add to blackboard chat history - // signal.description usually is "User: message" - // We'll parse it simply or use the whole string as content if format varies - // Assuming signal.description is the formatted message or we extract it. - // Based on previous logs, it looks like "Sender: message" - // Let's just use the description for now, or split it if possible. - // Actually `signal.content` might hold the raw message if available, but checking types it seems signal has description and properties. - // Let's assume description is "Sender: content" for now or just store it. - // A better way is to try to parse it if needed, but for now we trust `signal.description`. + private async handlePerceptionSignal(bot: MineflayerWithAgents, signal: PerceptionSignal): Promise { + this.log('INFO', `Brain: Received perception: ${signal.description}`) + if (signal.type === 'chat_message') { const parts = signal.description.split(': ') const sender = parts.length > 1 ? parts[0] : 'Unknown' const content = parts.length > 1 ? parts.slice(1).join(': ') : signal.description @@ -163,11 +143,33 @@ export class Brain { content, timestamp: Date.now(), }) + } + + await this.enqueueEvent(bot, { + type: 'perception', + payload: signal, + source: { + type: 'minecraft', + id: signal.sourceId ?? 'perception', + }, + timestamp: Date.now(), + }) + } + + public init(bot: MineflayerWithAgents): void { + this.log('INFO', 'Brain: Initializing...') + this.bot = bot + this.blackboard.update({ selfUsername: bot.username }) + + // Perception Signal Handler - Only process chat messages for now + this.deps.eventManager.on('perception', async (event) => { + const signal = event.payload + // Only handle chat messages in the deliberative layer + if (signal.type !== 'chat_message' && signal.type !== 'social_presence') + return try { - this.log('DEBUG', `Brain: About to enqueue chat event`) - await this.enqueueEvent(bot, event) - this.log('DEBUG', `Brain: Chat event enqueued successfully`) + await this.handlePerceptionSignal(bot, signal) } catch (err) { this.log('ERROR', `Brain: Failed to enqueue chat event`, { error: err }) @@ -247,6 +249,9 @@ export class Brain { this.updateDebugState() } + public destroy(): void { + } + // --- Event Queue Logic --- private async enqueueEvent(bot: MineflayerWithAgents, event: BotEvent): Promise { diff --git a/services/minecraft/src/cognitive/index.ts b/services/minecraft/src/cognitive/index.ts index 0d3ba8641..8c902c243 100644 --- a/services/minecraft/src/cognitive/index.ts +++ b/services/minecraft/src/cognitive/index.ts @@ -24,6 +24,7 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug const actionAgent = container.resolve('actionAgent') const chatAgent = container.resolve('chatAgent') const perceptionPipeline = container.resolve('perceptionPipeline') + const eventManager = container.resolve('eventManager') const brain = container.resolve('brain') const reflexManager = container.resolve('reflexManager') const taskExecutor = container.resolve('taskExecutor') @@ -47,8 +48,8 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug reflexManager.init(botWithAgents) brain.init(botWithAgents) - const ruleEngine = container.resolve('ruleEngine') - ruleEngine.init() + // Ensure RuleEngine is instantiated (Awilix is lazy). It subscribes to raw:* during construction init. + void container.resolve('ruleEngine') // Initialize perception pipeline (raw events + detectors) perceptionPipeline.init(botWithAgents) @@ -72,6 +73,17 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug // Resolve EventBus and subscribe to forward events to debug timeline const eventBus = container.resolve('eventBus') + + // Bridge selected EventBus signals into EventManager perception stream for Brain. + eventBus.subscribe('signal:social_presence', (event) => { + eventManager.emit({ + type: 'perception', + payload: event.payload as any, + source: { type: 'minecraft', id: 'eventBus' }, + timestamp: Date.now(), + }) + }) + eventBus.subscribe('*', (event) => { // Forward to debug service for timeline visualization DebugService.getInstance().emitTrace({ diff --git a/services/minecraft/src/cognitive/perception/mineflayer-perception-collector.ts b/services/minecraft/src/cognitive/perception/mineflayer-perception-collector.ts index ccf787379..28f8790a5 100644 --- a/services/minecraft/src/cognitive/perception/mineflayer-perception-collector.ts +++ b/services/minecraft/src/cognitive/perception/mineflayer-perception-collector.ts @@ -6,6 +6,7 @@ import type { FeltDamageTakenEvent, FeltItemCollectedEvent, HeardSoundEvent, + PlayerJoinedEvent, RawPerceptionEvent, SightedArmSwingEvent, SightedEntityMovedEvent, @@ -19,6 +20,8 @@ export class MineflayerPerceptionCollector { handler: (...args: any[]) => void }> = [] + private knownPlayerIds: Set = new Set() + private lastSelfHealth: number | null = null private lastStatsAt = 0 private stats: Record = {} @@ -37,6 +40,7 @@ export class MineflayerPerceptionCollector { this.lastSelfHealth = bot.bot.health this.lastStatsAt = Date.now() this.stats = {} + this.knownPlayerIds = this.snapshotKnownPlayers(bot) this.deps.logger.withFields({ maxDistance: this.deps.maxDistance }).log('MineflayerPerceptionCollector: init') @@ -73,6 +77,8 @@ export class MineflayerPerceptionCollector { this.onBot('entityMoved', entity => this.handleEntityMoved(entity)) this.onBot('entitySwingArm', entity => this.handleEntitySwingArm(entity)) this.onBot('entityUpdate', entity => this.handleEntityUpdate(entity)) + this.onBot('playerJoined', player => this.handlePlayerJoined(player)) + this.onBot('playerUpdated', () => this.handlePlayersMaybeChanged()) this.onBot('soundEffectHeard', (soundId, pos) => this.handleSoundHeard(soundId, pos)) this.onBot('health', () => this.handleHealthChange()) this.onBot('playerCollect', (collector, collected) => this.handleItemCollected(collector, collected)) @@ -230,6 +236,82 @@ export class MineflayerPerceptionCollector { this.emitEvent(event, 'felt.item_collected') } + private handlePlayerJoined(player: any): void { + if (!player) + return + + if (player.username === this.bot?.bot.username) + return + + const playerId = String(player.uuid ?? player.id ?? player.username ?? 'unknown') + if (this.knownPlayerIds.has(playerId)) + return + + this.knownPlayerIds.add(playerId) + + const event: PlayerJoinedEvent = { + modality: 'system', + kind: 'player_joined', + playerId, + displayName: player.username, + timestamp: Date.now(), + source: 'minecraft', + } + + this.emitEvent(event, 'system.player_joined') + } + + private handlePlayersMaybeChanged(): void { + const bot = this.bot + if (!bot) + return + + const current = this.snapshotKnownPlayers(bot) + + for (const playerId of current) { + if (this.knownPlayerIds.has(playerId)) + continue + + this.knownPlayerIds.add(playerId) + + const player = (bot.bot as any).players?.[playerId] + const username = player?.username + + const event: PlayerJoinedEvent = { + modality: 'system', + kind: 'player_joined', + playerId, + displayName: typeof username === 'string' ? username : undefined, + timestamp: Date.now(), + source: 'minecraft', + } + + this.emitEvent(event, 'system.player_joined') + } + } + + private snapshotKnownPlayers(bot: MineflayerWithAgents): Set { + const out = new Set() + const players = (bot.bot as any).players as Record | undefined + if (!players) + return out + + const selfUsername = bot.bot.username + + for (const [id, player] of Object.entries(players)) { + if (!id) + continue + + const username = player?.username + if (username && username === selfUsername) + continue + + out.add(String(id)) + } + + return out + } + // ======================================== // Validation Helpers // ======================================== diff --git a/services/minecraft/src/cognitive/perception/types/raw-events.ts b/services/minecraft/src/cognitive/perception/types/raw-events.ts index 70ddd4c18..67546c0ee 100644 --- a/services/minecraft/src/cognitive/perception/types/raw-events.ts +++ b/services/minecraft/src/cognitive/perception/types/raw-events.ts @@ -1,6 +1,6 @@ import type { Vec3 } from 'vec3' -export type PerceptionModality = 'sighted' | 'heard' | 'felt' +export type PerceptionModality = 'sighted' | 'heard' | 'felt' | 'system' export interface RawPerceptionEventBase { modality: PerceptionModality @@ -71,4 +71,13 @@ export interface FeltItemCollectedEvent extends RawPerceptionEventBase { export type FeltEvent = FeltDamageTakenEvent | FeltItemCollectedEvent -export type RawPerceptionEvent = SightedEvent | HeardEvent | FeltEvent +export interface PlayerJoinedEvent extends RawPerceptionEventBase { + modality: 'system' + kind: 'player_joined' + playerId: string + displayName?: string +} + +export type SystemEvent = PlayerJoinedEvent + +export type RawPerceptionEvent = SightedEvent | HeardEvent | FeltEvent | SystemEvent diff --git a/services/minecraft/src/cognitive/perception/types/signals.ts b/services/minecraft/src/cognitive/perception/types/signals.ts index d6d2e78d8..1415cb5ff 100644 --- a/services/minecraft/src/cognitive/perception/types/signals.ts +++ b/services/minecraft/src/cognitive/perception/types/signals.ts @@ -4,6 +4,7 @@ export type PerceptionSignalType | 'environmental_anomaly' // e.g. sudden loud sound | 'saliency_high' // generic high saliency event | 'social_gesture' // e.g. teabagging, waving + | 'social_presence' export interface PerceptionSignal { type: PerceptionSignalType diff --git a/services/minecraft/src/cognitive/rules/social/player-joined.yaml b/services/minecraft/src/cognitive/rules/social/player-joined.yaml new file mode 100644 index 000000000..ae7658cdc --- /dev/null +++ b/services/minecraft/src/cognitive/rules/social/player-joined.yaml @@ -0,0 +1,19 @@ +name: player-joined +version: 1 + +trigger: + modality: system + kind: player_joined + +accumulator: + threshold: 1 + window: 1s + +signal: + type: social_presence + description: 'Player {{ displayName }} joined the server' + confidence: 1.0 + metadata: + event: player_joined + playerId: '{{ playerId }}' + displayName: '{{ displayName }}'