feat: middleware
This commit is contained in:
@@ -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')
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user