feat(minecraft): event on player join, wire up eventbus

This commit is contained in:
Rin
2026-02-18 11:12:06 +08:00
committed by Neko Ayaka
parent 5b42001769
commit c4b35f650a
6 changed files with 158 additions and 30 deletions
@@ -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<PerceptionSignal>('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<void> {
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<PerceptionSignal>('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<void> {
+14 -2
View File
@@ -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({
@@ -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<string> = new Set()
private lastSelfHealth: number | null = null
private lastStatsAt = 0
private stats: Record<string, number> = {}
@@ -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<string> {
const out = new Set<string>()
const players = (bot.bot as any).players as Record<string, any> | 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
// ========================================
@@ -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
@@ -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
@@ -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 }}'