feat(minecraft): logging for perception pipeline

This commit is contained in:
Rin
2026-02-18 11:10:02 +08:00
committed by Neko Ayaka
parent 2d4eff7dcb
commit 9b53e4311b
4 changed files with 165 additions and 33 deletions
@@ -26,6 +26,9 @@ export type AttentionEventPayload = PlayerAttentionEventPayload | MobAttentionEv
export class AttentionDetector {
private readonly buckets = new Map<string, LeakyBucket>()
private lastStatsAt = 0
private emittedSinceStats: Record<string, number> = {}
private readonly movementState = new Map<
string,
{
@@ -68,6 +71,16 @@ export class AttentionDetector {
this.movementState.delete(id)
}
}
if (now - this.lastStatsAt >= 2000) {
this.deps.logger.withFields({
deltaMs,
...this.emittedSinceStats,
}).log('AttentionDetector: stats')
this.lastStatsAt = now
this.emittedSinceStats = {}
}
}
public ingest(event: RawPerceptionEvent): void {
@@ -216,6 +229,30 @@ export class AttentionDetector {
}
private emitAttention(payload: AttentionEventPayload): void {
const key = payload.kind === 'player'
? `emit.player.${payload.playerAction}`
: 'emit.mob'
this.emittedSinceStats[key] = (this.emittedSinceStats[key] ?? 0) + 1
if (payload.kind === 'player') {
this.deps.logger.withFields({
kind: payload.kind,
action: payload.playerAction,
playerName: payload.playerName,
distance: payload.distance,
hasLineOfSight: payload.hasLineOfSight,
}).log('AttentionDetector: emit')
}
else {
this.deps.logger.withFields({
kind: payload.kind,
mobName: payload.mobName,
action: payload.mobAction,
distance: payload.distance,
hasLineOfSight: payload.hasLineOfSight,
}).log('AttentionDetector: emit')
}
this.deps.eventManager.emit<AttentionEventPayload>({
type: 'perception',
payload,
@@ -23,6 +23,11 @@ export class MineflayerPerceptionCollector {
private readonly lastSneak = new Map<string, boolean>()
private lastSelfHealth: number | null = null
private lastStatsAt = 0
private stats: Record<string, number> = {}
private losSamples = 0
private losTotalMs = 0
constructor(
private readonly deps: {
logger: Logg
@@ -35,6 +40,13 @@ export class MineflayerPerceptionCollector {
this.bot = bot
this.lastSelfHealth = bot.bot.health
this.lastStatsAt = Date.now()
this.stats = {}
this.losSamples = 0
this.losTotalMs = 0
this.deps.logger.withFields({ maxDistance: this.deps.maxDistance }).log('MineflayerPerceptionCollector: init')
this.onBot('entityMoved', (entity: any) => {
const now = Date.now()
const dist = this.distanceTo(entity)
@@ -61,6 +73,8 @@ export class MineflayerPerceptionCollector {
}
this.deps.emitRaw(event)
this.bumpStat('sighted.entity_moved')
this.maybeLogStats()
})
this.onBot('entitySwingArm', (entity: any) => {
@@ -83,6 +97,8 @@ export class MineflayerPerceptionCollector {
}
this.deps.emitRaw(event)
this.bumpStat('sighted.arm_swing')
this.maybeLogStats()
})
this.onBot('entityUpdate', (entity: any) => {
@@ -119,6 +135,8 @@ export class MineflayerPerceptionCollector {
}
this.deps.emitRaw(event)
this.bumpStat('sighted.sneak_toggle')
this.maybeLogStats()
})
this.onBot('soundEffectHeard', (soundId: string, pos: Vec3) => {
@@ -167,6 +185,8 @@ export class MineflayerPerceptionCollector {
}
this.deps.emitRaw(event)
this.bumpStat('felt.damage_taken')
this.maybeLogStats()
})
// Felt: item collected (best-effort; depends on mineflayer version/events)
@@ -190,6 +210,8 @@ export class MineflayerPerceptionCollector {
}
this.deps.emitRaw(event)
this.bumpStat('felt.item_collected')
this.maybeLogStats()
})
this.onBot('entityCollect', (collector: any, collected: any) => {
@@ -212,6 +234,8 @@ export class MineflayerPerceptionCollector {
}
this.deps.emitRaw(event)
this.bumpStat('felt.item_collected')
this.maybeLogStats()
})
}
@@ -219,10 +243,12 @@ export class MineflayerPerceptionCollector {
if (!this.bot)
return
this.deps.logger.withFields({ listeners: this.listeners.length }).log('MineflayerPerceptionCollector: destroy')
for (const { event, handler } of this.listeners) {
try {
(this.bot.bot as any).off?.(event, handler);
(this.bot.bot as any).removeListener?.(event, handler)
(this.bot.bot as any).off?.(event, handler)
(this.bot.bot as any).removeListener?.(event, handler)
}
catch (err) {
this.deps.logger.withError(err as Error).error('MineflayerPerceptionCollector: failed to remove listener')
@@ -236,6 +262,29 @@ export class MineflayerPerceptionCollector {
this.bot = null
}
private bumpStat(key: string): void {
this.stats[key] = (this.stats[key] ?? 0) + 1
}
private maybeLogStats(): void {
const now = Date.now()
if (now - this.lastStatsAt < 2000)
return
const losAvgMs = this.losSamples > 0 ? this.losTotalMs / this.losSamples : 0
this.deps.logger.withFields({
...this.stats,
losSamples: this.losSamples,
losAvgMs,
}).log('MineflayerPerceptionCollector: stats')
this.lastStatsAt = now
this.stats = {}
this.losSamples = 0
this.losTotalMs = 0
}
private onBot(event: string, handler: (...args: any[]) => void): void {
if (!this.bot)
return
@@ -273,41 +322,52 @@ export class MineflayerPerceptionCollector {
if (!this.bot)
return false
const startedAt = Date.now()
try {
const canSee = (this.bot.bot as any).canSeeEntity
if (typeof canSee === 'function')
return !!canSee.call(this.bot.bot, entity)
}
catch { }
// Fallback ray-march; intentionally simple (we'll optimize later)
try {
const from = this.bot.bot.entity.position.offset(0, this.bot.bot.entity.height * 0.9, 0)
const to = entity.position.offset(0, entity.height * 0.9, 0)
const dir = to.minus(from)
const total = dir.norm()
if (total <= 0)
return true
const stepSize = 0.25
const steps = Math.ceil(total / stepSize)
const step = dir.normalize().scaled(stepSize)
let cur = from.clone()
for (let i = 0; i < steps; i++) {
cur = cur.plus(step)
const block = this.bot.bot.blockAt(cur)
if (!block)
continue
if (block.boundingBox === 'block' && !block.transparent)
return false
try {
const canSee = (this.bot.bot as any).canSeeEntity
if (typeof canSee === 'function')
return !!canSee.call(this.bot.bot, entity)
}
catch { }
return true
// Fallback ray-march; intentionally simple (we'll optimize later)
try {
const from = this.bot.bot.entity.position.offset(0, this.bot.bot.entity.height * 0.9, 0)
const to = entity.position.offset(0, entity.height * 0.9, 0)
const dir = to.minus(from)
const total = dir.norm()
if (total <= 0)
return true
const stepSize = 0.25
const steps = Math.ceil(total / stepSize)
const step = dir.normalize().scaled(stepSize)
let cur = from.clone()
for (let i = 0; i < steps; i++) {
cur = cur.plus(step)
const block = this.bot.bot.blockAt(cur)
if (!block)
continue
if (block.boundingBox === 'block' && !block.transparent)
return false
}
return true
}
catch {
return false
}
}
catch {
return false
finally {
const costMs = Date.now() - startedAt
if (costMs > 0) {
this.losSamples++
this.losTotalMs += costMs
}
}
}
}
@@ -14,6 +14,10 @@ export class PerceptionPipeline {
private collector: MineflayerPerceptionCollector | null = null
private initialized = false
private lastStatsAt = 0
private collectedSinceStats = 0
private processedSinceStats = 0
constructor(
private readonly deps: {
eventManager: EventManager
@@ -29,6 +33,12 @@ export class PerceptionPipeline {
public init(bot: MineflayerWithAgents): void {
this.initialized = true
this.lastStatsAt = Date.now()
this.collectedSinceStats = 0
this.processedSinceStats = 0
this.deps.logger.withFields({ maxDistance: 32 }).log('PerceptionPipeline: init')
this.collector = new MineflayerPerceptionCollector({
logger: this.deps.logger,
emitRaw: (event) => {
@@ -40,6 +50,7 @@ export class PerceptionPipeline {
}
public destroy(): void {
this.deps.logger.log('PerceptionPipeline: destroy')
this.collector?.destroy()
this.collector = null
this.buffer.clear()
@@ -50,15 +61,19 @@ export class PerceptionPipeline {
if (!this.initialized)
return
this.buffer.push(event)
this.collectedSinceStats++
}
public tick(deltaMs: number): void {
if (!this.initialized)
return
const startedAt = Date.now()
this.detector.tick(deltaMs)
const events = this.buffer.drain()
this.processedSinceStats += events.length
for (const event of events) {
try {
this.detector.ingest(event)
@@ -67,5 +82,21 @@ export class PerceptionPipeline {
this.deps.logger.withError(err as Error).error('PerceptionPipeline: detector error')
}
}
const now = Date.now()
if (now - this.lastStatsAt >= 2000) {
this.deps.logger.withFields({
deltaMs,
tickCostMs: now - startedAt,
queueDepth: this.buffer.size(),
drained: events.length,
collected: this.collectedSinceStats,
processed: this.processedSinceStats,
}).log('PerceptionPipeline: stats')
this.lastStatsAt = now
this.collectedSinceStats = 0
this.processedSinceStats = 0
}
}
}
@@ -7,6 +7,10 @@ export class RawEventBuffer {
this.queue.push(event)
}
public size(): number {
return this.queue.length
}
public drain(): RawPerceptionEvent[] {
if (this.queue.length === 0)
return []