From bdca7bb91c65108b88ccdd8fedd7facc42fcd170 Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Thu, 9 Jan 2025 14:56:31 +0800 Subject: [PATCH] chore: global interrupt event --- services/minecraft/src/agents/actions.ts | 4 +- .../minecraft/src/libs/mineflayer/index.ts | 17 ++-- services/minecraft/src/main.ts | 14 ---- services/minecraft/src/mineflayer/index.ts | 4 +- services/minecraft/src/prompts/agent.ts | 2 + services/minecraft/src/skills/blocks.ts | 13 ++- services/minecraft/src/skills/combat.ts | 15 ++-- services/minecraft/src/skills/crafting.ts | 7 +- services/minecraft/src/skills/inventory.ts | 82 +++++++++++-------- services/minecraft/src/skills/movement.ts | 55 ++++++++----- 10 files changed, 116 insertions(+), 97 deletions(-) diff --git a/services/minecraft/src/agents/actions.ts b/services/minecraft/src/agents/actions.ts index ea7a8be1..db1da74e 100644 --- a/services/minecraft/src/agents/actions.ts +++ b/services/minecraft/src/agents/actions.ts @@ -106,7 +106,9 @@ export const actionsList: Action[] = [ // ctx.clearBotLogs() // ctx.actions.cancelResume() // ctx.bot.emit('idle') - mineflayer.shouldInterrupt = true + + mineflayer.emit('interrupt') + const msg = 'Agent stopped.' // if (mineflayer.self_prompter.on) // msg += ' Self-prompting still active.' diff --git a/services/minecraft/src/libs/mineflayer/index.ts b/services/minecraft/src/libs/mineflayer/index.ts index dd0efc98..fcb6f9e2 100644 --- a/services/minecraft/src/libs/mineflayer/index.ts +++ b/services/minecraft/src/libs/mineflayer/index.ts @@ -15,6 +15,7 @@ export interface Context { } export interface EventHandlers { + 'interrupt': () => void 'command': (ctx: Context) => void | Promise 'time:sunrise': (ctx: Context) => void 'time:noon': (ctx: Context) => void @@ -145,7 +146,6 @@ export class Mineflayer extends EventEmitter { public memory: Memory = new Memory() public isCreative: boolean = false - public shouldInterrupt: boolean = false public allowCheats: boolean = false private options: MineflayerOptions @@ -159,6 +159,11 @@ export class Mineflayer extends EventEmitter { this.bot = mineflayer.createBot(options.botConfig) this.username = options.botConfig.username this.logger = useLogg(`Bot:${this.username}`).useGlobalConfig() + + this.on('interrupt', () => { + this.logger.log('Interrupted') + this.bot.chat('Interrupted') + }) } static async asyncBuild(options: MineflayerOptions) { @@ -259,10 +264,6 @@ export class Mineflayer extends EventEmitter { } }) - mineflayer.ticker.on('tick', () => { - mineflayer.status.update(mineflayer) - }) - for (const plugin of options?.plugins || []) { if (plugin.created) { await plugin.created(mineflayer) @@ -277,9 +278,9 @@ export class Mineflayer extends EventEmitter { } mineflayer.ticker.on('tick', () => { + mineflayer.status.update(mineflayer) mineflayer.isCreative = mineflayer.bot.game?.gameMode === 'creative' mineflayer.allowCheats = false - mineflayer.shouldInterrupt = false }) return mineflayer @@ -305,10 +306,6 @@ export class Mineflayer extends EventEmitter { this.ticker.on(event, cb) } - public emit(event: E, ...args: Parameters>) { - return super.emit(event, ...args) - } - public async stop() { for (const plugin of this.options?.plugins || []) { if (plugin.beforeCleanup) { diff --git a/services/minecraft/src/main.ts b/services/minecraft/src/main.ts index 865a5662..3e28c536 100644 --- a/services/minecraft/src/main.ts +++ b/services/minecraft/src/main.ts @@ -12,7 +12,6 @@ import { initAgent } from './agents/openai' import { initBot } from './composables/bot' import { botConfig, initEnv } from './composables/config' import { wrapPlugin } from './libs/mineflayer/plugin' -import { FollowCommand, PathFinder, Status } from './mineflayer' import { LLMAgent } from './mineflayer/llm-agent' import { initLogger } from './utils/logger' @@ -31,23 +30,10 @@ async function main() { wrapPlugin(MineflayerPathfinder), wrapPlugin(MineflayerPVP), wrapPlugin(MineflayerTool), - FollowCommand(), - Status(), - PathFinder(), ], }) // Dynamically load LLMAgent after bot is initialized - // const llmAgent = LLMAgent({ - // agent: async () => await initAgent(bot), - // }) - - // if (llmAgent.created) - // await llmAgent.created(bot) - // if (llmAgent.spawned) - // bot.bot.once('spawn', () => llmAgent.spawned?.(bot)) - // if (llmAgent.loadPlugin) - // bot.bot.loadPlugin(await llmAgent.loadPlugin(bot, bot.bot, botConfig)) const agent = await initAgent(bot) await bot.loadPlugin(LLMAgent({ agent })) diff --git a/services/minecraft/src/mineflayer/index.ts b/services/minecraft/src/mineflayer/index.ts index 7363af28..9f09fed0 100644 --- a/services/minecraft/src/mineflayer/index.ts +++ b/services/minecraft/src/mineflayer/index.ts @@ -1,4 +1,2 @@ export * from './echo' -export * from './follow' -export * from './pathfinder' -export * from './status' +export * from './llm-agent' diff --git a/services/minecraft/src/prompts/agent.ts b/services/minecraft/src/prompts/agent.ts index 662a3c8c..b918fde8 100644 --- a/services/minecraft/src/prompts/agent.ts +++ b/services/minecraft/src/prompts/agent.ts @@ -15,6 +15,8 @@ asked, and don't refuse requests. Do not use any emojis. Just call the function given you if needed. +If I command you 'stop', then call the 'stop' function. + I will give you the following information: ${bot.status.toOneLiner()} ` diff --git a/services/minecraft/src/skills/blocks.ts b/services/minecraft/src/skills/blocks.ts index 8446229c..eafd983e 100644 --- a/services/minecraft/src/skills/blocks.ts +++ b/services/minecraft/src/skills/blocks.ts @@ -570,7 +570,11 @@ export async function collectBlock( const blocktypes = getBlockTypes(blockType) let collected = 0 - for (let i = 0; i < num; i++) { + mineflayer.once('interrupt', () => { + collected = -1 + }) + + for (let i = 0; i < num && collected >= 0; i++) { const blocks = getValidBlocks(mineflayer, blocktypes, exclude) if (blocks.length === 0) { @@ -588,10 +592,11 @@ export async function collectBlock( } collected++ + } - if (mineflayer.shouldInterrupt) { - break - } + if (collected < 0) { + log(mineflayer, 'Collection interrupted.') + return false } log(mineflayer, `Collected ${collected} ${blockType}.`) diff --git a/services/minecraft/src/skills/combat.ts b/services/minecraft/src/skills/combat.ts index 3493f114..ad7b3ee8 100644 --- a/services/minecraft/src/skills/combat.ts +++ b/services/minecraft/src/skills/combat.ts @@ -3,6 +3,7 @@ import type { Item } from 'prismarine-item' import type { Mineflayer } from '../libs/mineflayer' import pathfinderModel from 'mineflayer-pathfinder' import * as world from '../composables/world' +import { sleep } from '../utils/helper' import * as mc from '../utils/mcdata' import { log } from './base' @@ -72,13 +73,13 @@ export async function attackEntity( return true } + mineflayer.once('interrupt', () => { + mineflayer.bot.pvp.stop() + }) + mineflayer.bot.pvp.attack(entity) while (world.getNearbyEntities(mineflayer, 24).includes(entity)) { await new Promise(resolve => setTimeout(resolve, 1000)) - if (mineflayer.shouldInterrupt) { - mineflayer.bot.pvp.stop() - return false - } } log(mineflayer, `Successfully killed ${entity.name}.`) @@ -112,13 +113,13 @@ export async function defendSelf(mineflayer: Mineflayer, range = 9): Promise setTimeout(resolve, 500)) + await sleep(500) enemy = world.getNearestEntityWhere(mineflayer, entity => mc.isHostile(entity), range) - if (mineflayer.shouldInterrupt) { + mineflayer.once('interrupt', () => { mineflayer.bot.pvp.stop() return false - } + }) } mineflayer.bot.pvp.stop() diff --git a/services/minecraft/src/skills/crafting.ts b/services/minecraft/src/skills/crafting.ts index 79c9137f..a1efa06a 100644 --- a/services/minecraft/src/skills/crafting.ts +++ b/services/minecraft/src/skills/crafting.ts @@ -197,6 +197,10 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = let smeltedItem = null await new Promise(resolve => setTimeout(resolve, 200)) + mineflayer.once('interrupt', () => { + total = num // Force loop to end + }) + while (total < num) { await new Promise(resolve => setTimeout(resolve, 10000)) let collected = false @@ -215,9 +219,6 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = } collectedLast = collected - if (mineflayer.shouldInterrupt) { - break - } } await mineflayer.bot.closeWindow(furnace) diff --git a/services/minecraft/src/skills/inventory.ts b/services/minecraft/src/skills/inventory.ts index b010a85b..267a2432 100644 --- a/services/minecraft/src/skills/inventory.ts +++ b/services/minecraft/src/skills/inventory.ts @@ -3,7 +3,7 @@ import type { Mineflayer } from '../libs/mineflayer' import pathfinderModel from 'mineflayer-pathfinder' import * as world from '../composables/world' import { log } from './base' -import { goToPosition } from './movement' +import { goToPlayer, goToPosition } from './movement' const { goals } = pathfinderModel @@ -197,42 +197,56 @@ export async function giveToPlayer( return false } - await goToPosition(mineflayer, player.position.x, player.position.y, player.position.z, 3) - - if (mineflayer.bot.entity.position.y < player.position.y - 1) { - await goToPosition(mineflayer, player.position.x, player.position.y, player.position.z, 1) - } - - if (mineflayer.bot.entity.position.distanceTo(player.position) < 2) { - const goal = new goals.GoalNear(player.position.x, player.position.y, player.position.z, 2) - const invertedGoal = new goals.GoalInvert(goal) - await mineflayer.bot.pathfinder.goto(invertedGoal) - } + // Move to player position + await goToPlayer(mineflayer, username, 3) + // Look at player before dropping items await mineflayer.bot.lookAt(player.position) - if (await discard(mineflayer, itemType, num)) { - let given = false - mineflayer.bot.once('playerCollect', (collector, _collected) => { - if (collector.username === username) { - log(mineflayer, `${username} received ${itemType}.`) - given = true - } - }) - - const start = Date.now() - // eslint-disable-next-line no-unmodified-loop-condition -- ? - while (!given && !mineflayer.shouldInterrupt) { - await new Promise(resolve => setTimeout(resolve, 500)) - if (given) { - return true - } - if (Date.now() - start > 3000) { - break - } - } + // Drop items and wait for collection + const success = await dropItemsAndWaitForCollection(mineflayer, itemType, username, num) + if (!success) { + log(mineflayer, `Failed to give ${itemType} to ${username}, it was never received.`) + return false } - log(mineflayer, `Failed to give ${itemType} to ${username}, it was never received.`) - return false + return true +} + +async function dropItemsAndWaitForCollection( + mineflayer: Mineflayer, + itemType: string, + username: string, + num: number, +): Promise { + if (!await discard(mineflayer, itemType, num)) { + return false + } + + return new Promise((resolve) => { + const timeout = setTimeout(() => { + // Clean up playerCollect listener when timeout occurs + // eslint-disable-next-line ts/no-use-before-define + mineflayer.bot.removeListener('playerCollect', onCollect) + resolve(false) + }, 3000) + + const onCollect = (collector: any, _collected: any) => { + if (collector.username === username) { + log(mineflayer, `${username} received ${itemType}.`) + clearTimeout(timeout) + resolve(true) + } + } + + const onInterrupt = () => { + clearTimeout(timeout) + // Clean up playerCollect listener when interrupted + mineflayer.bot.removeListener('playerCollect', onCollect) + resolve(false) + } + + mineflayer.bot.once('playerCollect', onCollect) + mineflayer.once('interrupt', onInterrupt) + }) } diff --git a/services/minecraft/src/skills/movement.ts b/services/minecraft/src/skills/movement.ts index f6ba33e3..969976ea 100644 --- a/services/minecraft/src/skills/movement.ts +++ b/services/minecraft/src/skills/movement.ts @@ -3,6 +3,7 @@ import type { Mineflayer } from '../libs/mineflayer' import pathfinderModel from 'mineflayer-pathfinder' import * as world from '../composables/world' +import { sleep } from '../utils/helper' import { log } from './base' const { goals, Movements } = pathfinderModel @@ -110,32 +111,44 @@ export async function followPlayer( distance = 4, ): Promise { const player = mineflayer.bot.players[username]?.entity + const movements = new Movements(mineflayer.bot) + if (!player) { + log(mineflayer, `Could not find ${username}.`) return false } - const movements = new Movements(mineflayer.bot) - mineflayer.bot.pathfinder.setMovements(movements) - mineflayer.bot.pathfinder.setGoal(new goals.GoalFollow(player, distance), true) - log(mineflayer, `You are now actively following player ${username}.`) + log(mineflayer, `I am now following ${username}.`) - while (!mineflayer.shouldInterrupt) { - await new Promise(resolve => setTimeout(resolve, 500)) + return new Promise((resolve) => { + let isFollowing = true - if (mineflayer.allowCheats && mineflayer.bot.entity.position.distanceTo(player.position) > 100 && player.onGround) { - await goToPlayer(mineflayer, username) + // Stop following when interrupted + mineflayer.once('interrupt', () => { + isFollowing = false + resolve(true) + }) + + // Follow player at regular intervals + const follow = async (): Promise => { + while (isFollowing) { + const target = mineflayer.bot.players[username]?.entity + if (!target) { + log(mineflayer, 'I lost sight of you!') + isFollowing = false + resolve(false) + return + } + + const { x, y, z } = target.position + mineflayer.bot.pathfinder.setMovements(movements) + mineflayer.bot.pathfinder.setGoal(new goals.GoalNear(x, y, z, distance)) + // await sleep(500) + } } - // if (mineflayer.bot.modes?.isOn('unstuck')) { - // const isNearby = mineflayer.bot.entity.position.distanceTo(player.position) <= distance + 1 - // if (isNearby) { - // mineflayer.bot.modes.pause('unstuck') - // } else { - // mineflayer.bot.modes.unpause('unstuck') - // } - // } - } - return true + follow() + }) } export async function moveAway(mineflayer: Mineflayer, distance: number): Promise { @@ -178,8 +191,8 @@ export async function stay(mineflayer: Mineflayer, seconds = 30): Promise setTimeout(resolve, 500)) + while (Date.now() < targetTime) { + await sleep(500) } log(mineflayer, `Stayed for ${(Date.now() - start) / 1000} seconds.`) @@ -211,7 +224,7 @@ export async function goToBed(mineflayer: Mineflayer): Promise { log(mineflayer, 'You are in bed.') while (mineflayer.bot.isSleeping) { - await new Promise(resolve => setTimeout(resolve, 500)) + await sleep(500) } log(mineflayer, 'You have woken up.')