feat: middleware

This commit is contained in:
RainbowBird
2025-01-06 15:51:30 +08:00
parent 8995c81083
commit 5dbf7669ba
5 changed files with 40 additions and 277 deletions
-263
View File
@@ -1,263 +0,0 @@
import type { Bot } from 'mineflayer'
import type { Item } from 'prismarine-item'
import type { Window } from 'prismarine-windows'
import type { ComponentLifecycle } from '../bot'
import { useLogg } from '@guiiai/logg'
const VALID_CHEST_TYPES = ['chest', 'ender_chest', 'trapped_chest'] as const
const VALID_FURNACE_TYPES = ['furnace', 'lit_furnace'] as const
const VALID_ENCHANT_TYPES = ['enchanting_table'] as const
interface ContainerContext {
window: Window
removeListeners: () => void
}
// Utility functions
function createItemUtils(bot: Bot) {
const itemToString = (item: Item | null): string => {
if (item)
return `${item.name} x ${item.count}`
return '(nothing)'
}
const findItemByName = (items: Item[], name: string): Item | null =>
items.find(item => item?.name === name) || null
const findItemByType = (items: Item[], type: number): Item | null =>
items.find(item => item?.type === type) || null
const listItems = (items = bot.inventory.items()): void => {
const output = items.map(itemToString).join(', ')
bot.chat(output || 'empty')
}
return {
itemToString,
findItemByName,
findItemByType,
listItems,
}
}
// Container management
function createContainerManager(bot: Bot, logger: ReturnType<typeof useLogg>) {
const setupContainerListeners = (
window: Window,
onChat: (username: string, message: string) => void,
type = 'container',
): () => void => {
const updateHandler = (slot: number, oldItem: Item, newItem: Item) => {
bot.chat(`${type} update: ${itemToString(oldItem)} -> ${itemToString(newItem)} (slot: ${slot})`)
}
const closeHandler = () => {
bot.chat(`${type} closed`)
}
window.on('updateSlot', updateHandler)
window.on('close', closeHandler)
bot.on('chat', onChat)
return () => {
window.removeListener('updateSlot', updateHandler)
window.removeListener('close', closeHandler)
bot.removeListener('chat', onChat)
}
}
const findNearbyBlock = (types: readonly string[], maxDistance = 6): ReturnType<Bot['findBlock']> => {
return bot.findBlock({
matching: types
.filter(name => bot.registry.blocksByName[name] !== undefined)
.map(name => bot.registry.blocksByName[name].id),
maxDistance,
})
}
return {
setupContainerListeners,
findNearbyBlock,
}
}
// Chest functions
function createChestManager(bot: Bot, logger: ReturnType<typeof useLogg>, utils: ReturnType<typeof createItemUtils>) {
const { itemToString, findItemByName, listItems } = utils
const handleChestCommands = async (window: Window, command: string[]): Promise<void> => {
switch (true) {
case /^close$/.test(command[0]): {
window.close()
break
}
case /^withdraw \d+ \w+$/.test(command.join(' ')): {
const amount = Number.parseInt(command[1], 10)
const name = command[2]
const item = findItemByName(window.containerItems(), name)
if (!item) {
bot.chat(`unknown item ${name}`)
return
}
try {
await window.withdraw(item.type, null, amount)
bot.chat(`withdrew ${amount} ${item.name}`)
}
catch (err) {
bot.chat(`unable to withdraw ${amount} ${item.name}`)
}
break
}
case /^deposit \d+ \w+$/.test(command.join(' ')): {
const amount = Number.parseInt(command[1], 10)
const name = command[2]
const item = findItemByName(window.items(), name)
if (!item) {
bot.chat(`unknown item ${name}`)
return
}
try {
await window.deposit(item.type, null, amount)
bot.chat(`deposited ${amount} ${item.name}`)
}
catch (err) {
bot.chat(`unable to deposit ${amount} ${item.name}`)
}
break
}
}
}
const openChest = async (minecart = false): Promise<ContainerContext | null> => {
let target
if (minecart) {
target = Object.values(bot.entities)
.find(e => e.entityType === bot.registry.entitiesByName.chest_minecart
&& bot.entity.position.distanceTo(e.position) < 3)
if (!target) {
bot.chat('no chest minecart found')
return null
}
}
else {
target = bot.findBlock({
matching: VALID_CHEST_TYPES.map(name => bot.registry.blocksByName[name].id),
maxDistance: 6,
})
if (!target) {
bot.chat('no chest found')
return null
}
}
try {
const window = await bot.openContainer(target)
const onChat = (username: string, message: string) => {
if (username === bot.username)
return
handleChestCommands(window, message.split(' '))
}
const removeListeners = setupContainerListeners(window, onChat, 'chest')
listItems(window.containerItems())
return { window, removeListeners }
}
catch (err) {
logger.error('Failed to open chest', err)
bot.chat('Failed to open chest')
return null
}
}
return {
openChest,
}
}
// Furnace functions
function createFurnaceManager(bot: Bot, logger: ReturnType<typeof useLogg>, containerManager: ReturnType<typeof createContainerManager>) {
const { findNearbyBlock } = containerManager
const openFurnace = async (): Promise<void> => {
const furnaceBlock = findNearbyBlock(VALID_FURNACE_TYPES)
if (!furnaceBlock) {
bot.chat('no furnace found')
return
}
try {
const furnace = await bot.openFurnace(furnaceBlock)
let output = ''
output += `input: ${itemToString(furnace.inputItem())}, `
output += `fuel: ${itemToString(furnace.fuelItem())}, `
output += `output: ${itemToString(furnace.outputItem())}`
bot.chat(output)
furnace.on('update', () => {
logger.debug(`fuel: ${Math.round(furnace.fuel * 100)}% progress: ${Math.round(furnace.progress * 100)}%`)
})
// Setup furnace command handlers...
}
catch (err) {
logger.error('Failed to open furnace', err)
bot.chat('Failed to open furnace')
}
}
return {
openFurnace,
}
}
export function createChestComponent(bot: Bot): ComponentLifecycle {
const logger = useLogg('chest').useGlobalConfig()
logger.log('Loading chest component')
const utils = createItemUtils(bot)
const containerManager = createContainerManager(bot, logger)
const chestManager = createChestManager(bot, logger, utils)
const furnaceManager = createFurnaceManager(bot, logger, containerManager)
// Main chat handler
const onChat = async (username: string, message: string): Promise<void> => {
if (username === bot.username)
return
switch (true) {
case /^list$/.test(message):
utils.listItems()
break
case /^chest$/.test(message):
await chestManager.openChest(false)
break
case /^chestminecart$/.test(message):
await chestManager.openChest(true)
break
case /^furnace$/.test(message):
await furnaceManager.openFurnace()
break
}
}
// Setup event listeners
bot.on('chat', onChat)
bot.on('experience', () => {
bot.chat(`I am level ${bot.experience.level}`)
})
// Cleanup function
return {
cleanup: () => {
bot.removeListener('chat', onChat)
logger.log('Chest component cleaned up')
},
}
}
+3 -5
View File
@@ -1,16 +1,14 @@
import type { ComponentLifecycle, Context } from '../bot'
import { useLogg } from '@guiiai/logg'
import { formBotChat } from 'src/middlewares/chat'
const logger = useLogg('echo').useGlobalConfig()
export function createEchoComponent(ctx: Context): ComponentLifecycle {
const onChat = (username: string, message: string) => {
if (username === ctx.bot.username)
return
const onChat = formBotChat(ctx, (username, message) => {
logger.withFields({ username, message }).log('Chat message received')
ctx.bot.chat(message)
}
})
ctx.bot.on('chat', onChat)
+4 -3
View File
@@ -1,6 +1,7 @@
import type { ComponentLifecycle, Context } from '../ctx.bot'
import type { ComponentLifecycle, Context } from '../bot'
import { useLogg } from '@guiiai/logg'
import { goals, Movements, pathfinder } from 'mineflayer-pathfinder'
import { formBotChat } from 'src/middlewares/chat'
export function createFollowComponent(ctx: Context): ComponentLifecycle {
const RANGE_GOAL = 2 // get within this radius of the player
@@ -30,7 +31,7 @@ export function createFollowComponent(ctx: Context): ComponentLifecycle {
ctx.bot.pathfinder.setGoal(new goals.GoalNear(playerX, playerY, playerZ, RANGE_GOAL))
}
const onChat = (username: string, message: string) => {
const onChat = formBotChat(ctx, (username, message) => {
if (username === ctx.bot.username)
return
@@ -44,7 +45,7 @@ export function createFollowComponent(ctx: Context): ComponentLifecycle {
logger.log('Stopping follow')
ctx.bot.pathfinder.stop()
}
}
})
ctx.bot.once('spawn', () => {
defaultMove = new Movements(ctx.bot)
@@ -1,8 +1,7 @@
// This is an example that uses mineflayer-pathfinder to showcase how simple it is to walk to goals
import type { ComponentLifecycle, Context } from '../bot'
import { useLogg } from '@guiiai/logg'
import { goals, Movements, pathfinder } from 'mineflayer-pathfinder'
import { formBotChat } from 'src/middlewares/chat'
export function createPathFinderComponent(ctx: Context): ComponentLifecycle {
const RANGE_GOAL = 1 // get within this radius of the player
@@ -14,9 +13,7 @@ export function createPathFinderComponent(ctx: Context): ComponentLifecycle {
let defaultMove: Movements
const onChat = (username: string, message: string) => {
if (username === ctx.bot.username)
return
const onChat = formBotChat(ctx, (username, message) => {
if (message !== 'come')
return
@@ -31,7 +28,7 @@ export function createPathFinderComponent(ctx: Context): ComponentLifecycle {
ctx.bot.pathfinder.setMovements(defaultMove)
ctx.bot.pathfinder.setGoal(new goals.GoalNear(playerX, playerY, playerZ, RANGE_GOAL))
}
})
ctx.bot.once('spawn', () => {
defaultMove = new Movements(ctx.bot)
@@ -0,0 +1,30 @@
import type { Entity } from 'prismarine-entity'
import type { Context } from 'src/bot'
// TODO: need to be refactored
interface ChatContext {
fromUsername?: string
fromEntity?: Entity
fromMessage?: string
isBot: () => boolean
isCommand: () => boolean
}
export function newChatContext(ctx: Context, username: string, message: string): ChatContext {
return {
fromUsername: username,
fromEntity: ctx.bot.entity,
fromMessage: message,
isBot: () => username === ctx.bot.username,
isCommand: () => message.startsWith('#'),
}
}
export function formBotChat(ctx: Context, cb: (username: string, message: string) => void) {
return (username: string, message: string) => {
if (ctx.bot.username === username)
return
cb(username, message)
}
}