From a4ad2b698b48ed40c01387deed00a14a4055188d Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Fri, 10 Jan 2025 00:43:14 +0800 Subject: [PATCH] refactor: split libs --- .../minecraft/src/libs/mineflayer/action.ts | 11 + .../src/libs/mineflayer/components.ts | 29 ++ .../minecraft/src/libs/mineflayer/core.ts | 353 +++++++++++++ .../minecraft/src/libs/mineflayer/health.ts | 9 + .../minecraft/src/libs/mineflayer/index.ts | 484 +----------------- .../src/libs/mineflayer/interfaces.ts | 3 + .../minecraft/src/libs/mineflayer/memory.ts | 12 + .../minecraft/src/libs/mineflayer/status.ts | 46 ++ .../minecraft/src/libs/mineflayer/types.ts | 19 + 9 files changed, 494 insertions(+), 472 deletions(-) create mode 100644 services/minecraft/src/libs/mineflayer/action.ts create mode 100644 services/minecraft/src/libs/mineflayer/components.ts create mode 100644 services/minecraft/src/libs/mineflayer/core.ts create mode 100644 services/minecraft/src/libs/mineflayer/health.ts create mode 100644 services/minecraft/src/libs/mineflayer/interfaces.ts create mode 100644 services/minecraft/src/libs/mineflayer/memory.ts create mode 100644 services/minecraft/src/libs/mineflayer/status.ts create mode 100644 services/minecraft/src/libs/mineflayer/types.ts diff --git a/services/minecraft/src/libs/mineflayer/action.ts b/services/minecraft/src/libs/mineflayer/action.ts new file mode 100644 index 000000000..5cf278e5b --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/action.ts @@ -0,0 +1,11 @@ +import type { z } from 'zod' +import type { Mineflayer } from './core' + +type ActionResult = string | Promise + +export interface Action { + readonly name: string + readonly description: string + readonly schema: z.ZodObject + readonly perform: (mineflayer: Mineflayer) => (...args: any[]) => ActionResult +} diff --git a/services/minecraft/src/libs/mineflayer/components.ts b/services/minecraft/src/libs/mineflayer/components.ts new file mode 100644 index 000000000..4d96bddaf --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/components.ts @@ -0,0 +1,29 @@ +import type { Handler } from './types' + +import { useLogg } from '@guiiai/logg' + +export class Components { + private components: Map = new Map() + private logger: ReturnType + + constructor() { + this.logger = useLogg('Components').useGlobalConfig() + } + + register(componentName: string, component: Handler) { + this.components.set(componentName, component) + } + + get(componentName: string) { + return this.components.get(componentName) + } + + list() { + return Array.from(this.components.keys()) + } + + cleanup() { + this.logger.log('Cleaning up components') + this.components.clear() + } +} diff --git a/services/minecraft/src/libs/mineflayer/core.ts b/services/minecraft/src/libs/mineflayer/core.ts new file mode 100644 index 000000000..4bd2acf46 --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/core.ts @@ -0,0 +1,353 @@ +import type { Bot, BotOptions } from 'mineflayer' +import type { MineflayerPlugin } from './plugin' +import type { EventHandlers, EventsHandler } from './types' +import { useLogg } from '@guiiai/logg' +import EventEmitter from 'eventemitter3' +import mineflayer from 'mineflayer' +import { parseCommand } from './command' +import { Components } from './components' +import { Health } from './health' +import { Memory } from './memory' +import { formBotChat } from './message' +import { Status } from './status' +import { Ticker, type TickEvents, type TickEventsHandler } from './ticker' + +export interface MineflayerOptions { + botConfig: BotOptions + plugins?: Array +} + +export class Mineflayer extends EventEmitter { + public bot: Bot + public username: string + public health: Health = new Health() + public ready: boolean = false + public components: Components = new Components() + public status: Status = new Status() + public memory: Memory = new Memory() + + public isCreative: boolean = false + public allowCheats: boolean = false + + private options: MineflayerOptions + private logger: ReturnType + private commands: Map> = new Map() + private ticker: Ticker = new Ticker() + + constructor(options: MineflayerOptions) { + super() + this.options = options + 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') + }) + } + + public static async asyncBuild(options: MineflayerOptions) { + const mineflayer = new Mineflayer(options) + + mineflayer.bot.on('messagestr', async (message, _, jsonMsg) => { + // jsonMsg.translate: + // - death.attack.player + // message: + // - was slain by + // - drowned + if (jsonMsg.translate && jsonMsg.translate.startsWith('death') && message.startsWith(mineflayer.username)) { + const deathPos = mineflayer.bot.entity.position + + // mineflayer.memory_bank.rememberPlace('last_death_position', deathPos.x, deathPos.y, deathPos.z) + let deathPosStr: string | undefined + if (deathPos) { + deathPosStr = `x: ${deathPos.x.toFixed(2)}, y: ${deathPos.y.toFixed(2)}, z: ${deathPos.x.toFixed(2)}` + } + + const dimension = mineflayer.bot.game.dimension + await mineflayer.handleMessage('system', `You died at position ${deathPosStr || 'unknown'} in the ${dimension} dimension with the final message: '${message}'. Your place of death has been saved as 'last_death_position' if you want to return. Previous actions were stopped and you have re-spawned.`) + } + }) + + mineflayer.bot.once('resourcePack', () => { + mineflayer.bot.acceptResourcePack() + }) + + mineflayer.bot.on('time', () => { + if (mineflayer.bot.time.timeOfDay === 0) + mineflayer.emit('time:sunrise', { time: mineflayer.bot.time.timeOfDay }) + else if (mineflayer.bot.time.timeOfDay === 6000) + mineflayer.emit('time:noon', { time: mineflayer.bot.time.timeOfDay }) + else if (mineflayer.bot.time.timeOfDay === 12000) + mineflayer.emit('time:sunset', { time: mineflayer.bot.time.timeOfDay }) + else if (mineflayer.bot.time.timeOfDay === 18000) + mineflayer.emit('time:midnight', { time: mineflayer.bot.time.timeOfDay }) + }) + + mineflayer.bot.on('health', () => { + mineflayer.logger.withFields({ + health: mineflayer.health.value, + lastDamageTime: mineflayer.health.lastDamageTime, + lastDamageTaken: mineflayer.health.lastDamageTaken, + previousHealth: mineflayer.bot.health, + }).log('Health updated') + + if (mineflayer.bot.health < mineflayer.health.value) { + mineflayer.health.lastDamageTime = Date.now() + mineflayer.health.lastDamageTaken = mineflayer.health.value - mineflayer.bot.health + } + + mineflayer.health.value = mineflayer.bot.health + }) + + mineflayer.bot.once('spawn', () => { + mineflayer.ready = true + mineflayer.logger.log('Bot ready') + }) + + mineflayer.bot.on('death', () => { + mineflayer.logger.error('Bot died') + }) + + mineflayer.bot.on('kicked', (reason: string) => { + mineflayer.logger.withFields({ reason }).error('Bot was kicked') + }) + + mineflayer.bot.on('end', (reason) => { + mineflayer.logger.withFields({ reason }).log('Bot ended') + + // Try to reconnect after 5 seconds + setTimeout(async () => { + try { + await mineflayer.bot.connect(options.botConfig) + mineflayer.logger.log('Reconnected successfully') + } + catch (err) { + mineflayer.logger.errorWithError('Failed to reconnect:', err) + } + }, 5000) + }) + + mineflayer.bot.on('error', (err: Error) => { + mineflayer.logger.errorWithError('Bot error:', err) + }) + + mineflayer.bot.on('spawn', () => { + mineflayer.bot.on('chat', mineflayer.handleCommand()) + }) + + mineflayer.bot.on('spawn', async () => { + for (const plugin of options?.plugins || []) { + if (plugin.spawned) { + await plugin.spawned(mineflayer) + } + } + }) + + for (const plugin of options?.plugins || []) { + if (plugin.created) { + await plugin.created(mineflayer) + } + } + + // Load Plugins + for (const plugin of options?.plugins || []) { + if (plugin.loadPlugin) { + mineflayer.bot.loadPlugin(await plugin.loadPlugin(mineflayer, mineflayer.bot, options.botConfig)) + } + } + + mineflayer.ticker.on('tick', () => { + mineflayer.status.update(mineflayer) + mineflayer.isCreative = mineflayer.bot.game?.gameMode === 'creative' + mineflayer.allowCheats = false + }) + + return mineflayer + } + + public async loadPlugin(plugin: MineflayerPlugin) { + if (plugin.created) + await plugin.created(this) + + if (plugin.loadPlugin) { + this.bot.loadPlugin(await plugin.loadPlugin(this, this.bot, this.options.botConfig)) + } + + if (plugin.spawned) + this.bot.once('spawn', () => plugin.spawned?.(this)) + } + + public onCommand(commandName: string, cb: EventsHandler<'command'>) { + this.commands.set(commandName, cb) + } + + public onTick(event: TickEvents, cb: TickEventsHandler) { + this.ticker.on(event, cb) + } + + public async stop() { + for (const plugin of this.options?.plugins || []) { + if (plugin.beforeCleanup) { + await plugin.beforeCleanup(this) + } + } + this.components.cleanup() + this.bot.removeListener('chat', this.handleCommand()) + this.bot.end() + this.removeAllListeners() + } + + private handleCommand() { + return formBotChat(this.username, (sender, message) => { + const { isCommand, command, args } = parseCommand(sender, message) + + if (!isCommand) + return + + // Remove the # prefix from command + const cleanCommand = command.slice(1) + this.logger.withFields({ sender, command: cleanCommand, args }).log('Command received') + + const handler = this.commands.get(cleanCommand) + if (handler) { + handler({ time: this.bot.time.timeOfDay, command: { sender, isCommand, command: cleanCommand, args } }) + return + } + + // Built-in commands + switch (cleanCommand) { + case 'help': { + const commandList = Array.from(this.commands.keys()).concat(['help']) + this.bot.chat(`Available commands: ${commandList.map(cmd => `#${cmd}`).join(', ')}`) + break + } + default: + this.bot.chat(`Unknown command: ${cleanCommand}`) + } + }) + } + + private async handleMessage(_source: string, _message: string, _maxResponses: number = Infinity) { + // if (!source || !message) { + // console.warn('Received empty message from', source); + // return false; + // } + + // let used_command = false; + // if (maxResponses === null) { + // maxResponses = settings.max_commands === -1 ? Infinity : settings.max_commands; + // } + // if (maxResponses === -1) { + // maxResponses = Infinity; + // } + + // const self_prompt = source === 'system' || source === ctx.botName; + // const from_other_bot = convoManager.isOtherAgent(source); + + // if (!self_prompt && !from_other_bot) { // from user, check for forced commands + // const user_command_name = containsCommand(message); + // if (user_command_name) { + // if (!commandExists(user_command_name)) { + // this.routeResponse(source, `Command '${user_command_name}' does not exist.`); + // return false; + // } + // this.routeResponse(source, `*${source} used ${user_command_name.substring(1)}*`); + // if (user_command_name === '!newAction') { + // // all user-initiated commands are ignored by the bot except for this one + // // add the preceding message to the history to give context for newAction + // this.history.add(source, message); + // } + // let execute_res = await executeCommand(this, message); + // if (execute_res) + // this.routeResponse(source, execute_res); + // return true; + // } + // } + + // if (from_other_bot) + // this.last_sender = source; + + // // Now translate the message + // message = await handleEnglishTranslation(message); + // console.log('received message from', source, ':', message); + + // const checkInterrupt = () => this.self_prompter.shouldInterrupt(self_prompt) || this.shut_up || convoManager.responseScheduledFor(source); + + // let behavior_log = this.bot.modes.flushBehaviorLog(); + // if (behavior_log.trim().length > 0) { + // const MAX_LOG = 500; + // if (behavior_log.length > MAX_LOG) { + // behavior_log = '...' + behavior_log.substring(behavior_log.length - MAX_LOG); + // } + // behavior_log = 'Recent behaviors log: \n' + behavior_log.substring(behavior_log.indexOf('\n')); + // await this.history.add('system', behavior_log); + // } + + // // Handle other user messages + // await this.history.add(source, message); + // this.history.save(); + + // if (!self_prompt && this.self_prompter.on) // message is from user during self-prompting + // maxResponses = 1; // force only respond to this message, then let self-prompting take over + // for (let i=0; i 0) + // chat_message = `${pre_message} ${chat_message}`; + // this.routeResponse(source, chat_message); + // } + + // let execute_res = await executeCommand(this, res); + + // console.log('Agent executed:', command_name, 'and got:', execute_res); + // used_command = true; + + // if (execute_res) + // this.history.add('system', execute_res); + // else + // break; + // } + // else { // conversation response + // this.history.add(this.name, res); + // this.routeResponse(source, res); + // break; + // } + + // this.history.save(); + // } + + // return used_command; + } +} diff --git a/services/minecraft/src/libs/mineflayer/health.ts b/services/minecraft/src/libs/mineflayer/health.ts new file mode 100644 index 000000000..fb85b3b1e --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/health.ts @@ -0,0 +1,9 @@ +export class Health { + public value: number + public lastDamageTime?: number + public lastDamageTaken?: number + + constructor() { + this.value = 20 + } +} diff --git a/services/minecraft/src/libs/mineflayer/index.ts b/services/minecraft/src/libs/mineflayer/index.ts index 8f3573a1f..d8989b083 100644 --- a/services/minecraft/src/libs/mineflayer/index.ts +++ b/services/minecraft/src/libs/mineflayer/index.ts @@ -1,472 +1,12 @@ -import type { Bot, BotOptions } from 'mineflayer' -import type { Message } from 'neuri/openai' -import type { z } from 'zod' -import type { MineflayerPlugin } from './plugin' -import { useLogg } from '@guiiai/logg' -import EventEmitter from 'eventemitter3' -import mineflayer from 'mineflayer' -import { type CommandContext, parseCommand } from './command' -import { formBotChat } from './message' -import { Ticker, type TickEvents, type TickEventsHandler } from './ticker' - -export interface Context { - time: number - command?: CommandContext -} - -export interface EventHandlers { - 'interrupt': () => void - 'command': (ctx: Context) => void | Promise - 'time:sunrise': (ctx: Context) => void - 'time:noon': (ctx: Context) => void - 'time:sunset': (ctx: Context) => void - 'time:midnight': (ctx: Context) => void -} - -export type Events = keyof EventHandlers -export type EventsHandler = EventHandlers[K] - -export type Handler = (ctx: Context) => void | Promise - -export class Health { - public value: number - public lastDamageTime?: number - public lastDamageTaken?: number - - constructor() { - this.value = 20 - } -} - -interface OneLinerable { - toOneLiner: () => string -} - -export class Status implements OneLinerable { - public position: string - public health: string - public weather: string - public timeOfDay: string - - constructor() { - this.position = '' - this.health = '' - this.weather = '' - this.timeOfDay = '' - } - - public update(mineflayer: Mineflayer) { - if (!mineflayer.ready) - return - - Object.assign(this, Status.from(mineflayer)) - } - - static from(mineflayer: Mineflayer): Status { - if (!mineflayer.ready) - return new Status() - - const pos = mineflayer.bot.entity.position - const weather = mineflayer.bot.isRaining ? 'Rain' : mineflayer.bot.thunderState ? 'Thunderstorm' : 'Clear' - const timeOfDay = mineflayer.bot.time.timeOfDay < 6000 - ? 'Morning' - : mineflayer.bot.time.timeOfDay < 12000 ? 'Afternoon' : 'Night' - - const status = new Status() - status.position = `x: ${pos.x.toFixed(2)}, y: ${pos.y.toFixed(2)}, z: ${pos.z.toFixed(2)}` - status.health = `${Math.round(mineflayer.bot.health)} / 20` - status.weather = weather - status.timeOfDay = timeOfDay - - return status - } - - public toOneLiner(): string { - return Object.entries(this).map(([key, value]) => `${key}: ${value}`).join('\n') - } -} - -type ActionResult = string | Promise - -export interface Action { - readonly name: string - readonly description: string - readonly schema: z.ZodObject - readonly perform: (mineflayer: Mineflayer) => (...args: any[]) => ActionResult -} - -export class Memory { - public chatHistory: Message[] - public actions: Action[] - - constructor() { - this.chatHistory = [] - this.actions = [] - } -} - -export class Components { - private components: Map = new Map() - private logger: ReturnType - - constructor() { - this.logger = useLogg('Components').useGlobalConfig() - } - - register(componentName: string, component: Handler) { - this.components.set(componentName, component) - } - - get(componentName: string) { - return this.components.get(componentName) - } - - list() { - return Array.from(this.components.keys()) - } - - cleanup() { - this.logger.log('Cleaning up components') - this.components.clear() - } -} - -export interface MineflayerOptions { - botConfig: BotOptions - plugins?: Array -} - -export class Mineflayer extends EventEmitter { - public bot: Bot - public username: string - public health: Health = new Health() - public ready: boolean = false - public components: Components = new Components() - public status: Status = new Status() - public memory: Memory = new Memory() - - public isCreative: boolean = false - public allowCheats: boolean = false - - private options: MineflayerOptions - private logger: ReturnType - private commands: Map> = new Map() - private ticker: Ticker = new Ticker() - - constructor(options: MineflayerOptions) { - super() - this.options = options - 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) { - const mineflayer = new Mineflayer(options) - - mineflayer.bot.on('messagestr', async (message, _, jsonMsg) => { - // jsonMsg.translate: - // - death.attack.player - // message: - // - was slain by - // - drowned - if (jsonMsg.translate && jsonMsg.translate.startsWith('death') && message.startsWith(mineflayer.username)) { - const deathPos = mineflayer.bot.entity.position - - // mineflayer.memory_bank.rememberPlace('last_death_position', deathPos.x, deathPos.y, deathPos.z) - let deathPosStr: string | undefined - if (deathPos) { - deathPosStr = `x: ${deathPos.x.toFixed(2)}, y: ${deathPos.y.toFixed(2)}, z: ${deathPos.x.toFixed(2)}` - } - - const dimension = mineflayer.bot.game.dimension - await mineflayer.handleMessage('system', `You died at position ${deathPosStr || 'unknown'} in the ${dimension} dimension with the final message: '${message}'. Your place of death has been saved as 'last_death_position' if you want to return. Previous actions were stopped and you have re-spawned.`) - } - }) - - mineflayer.bot.once('resourcePack', () => { - mineflayer.bot.acceptResourcePack() - }) - - mineflayer.bot.on('time', () => { - if (mineflayer.bot.time.timeOfDay === 0) - mineflayer.emit('time:sunrise', { time: mineflayer.bot.time.timeOfDay }) - else if (mineflayer.bot.time.timeOfDay === 6000) - mineflayer.emit('time:noon', { time: mineflayer.bot.time.timeOfDay }) - else if (mineflayer.bot.time.timeOfDay === 12000) - mineflayer.emit('time:sunset', { time: mineflayer.bot.time.timeOfDay }) - else if (mineflayer.bot.time.timeOfDay === 18000) - mineflayer.emit('time:midnight', { time: mineflayer.bot.time.timeOfDay }) - }) - - mineflayer.bot.on('health', () => { - mineflayer.logger.withFields({ - health: mineflayer.health.value, - lastDamageTime: mineflayer.health.lastDamageTime, - lastDamageTaken: mineflayer.health.lastDamageTaken, - previousHealth: mineflayer.bot.health, - }).log('Health updated') - - if (mineflayer.bot.health < mineflayer.health.value) { - mineflayer.health.lastDamageTime = Date.now() - mineflayer.health.lastDamageTaken = mineflayer.health.value - mineflayer.bot.health - } - - mineflayer.health.value = mineflayer.bot.health - }) - - mineflayer.bot.once('spawn', () => { - mineflayer.ready = true - mineflayer.logger.log('Bot ready') - }) - - mineflayer.bot.on('death', () => { - mineflayer.logger.error('Bot died') - }) - - mineflayer.bot.on('kicked', (reason: string) => { - mineflayer.logger.withFields({ reason }).error('Bot was kicked') - }) - - mineflayer.bot.on('end', (reason) => { - mineflayer.logger.withFields({ reason }).log('Bot ended') - - // Try to reconnect after 5 seconds - setTimeout(async () => { - try { - await mineflayer.bot.connect(options.botConfig) - mineflayer.logger.log('Reconnected successfully') - } - catch (err) { - mineflayer.logger.errorWithError('Failed to reconnect:', err) - } - }, 5000) - }) - - mineflayer.bot.on('error', (err: Error) => { - mineflayer.logger.errorWithError('Bot error:', err) - }) - - mineflayer.bot.on('spawn', () => { - mineflayer.bot.on('chat', mineflayer.handleCommand()) - }) - - mineflayer.bot.on('spawn', async () => { - for (const plugin of options?.plugins || []) { - if (plugin.spawned) { - await plugin.spawned(mineflayer) - } - } - }) - - for (const plugin of options?.plugins || []) { - if (plugin.created) { - await plugin.created(mineflayer) - } - } - - // Load Plugins - for (const plugin of options?.plugins || []) { - if (plugin.loadPlugin) { - mineflayer.bot.loadPlugin(await plugin.loadPlugin(mineflayer, mineflayer.bot, options.botConfig)) - } - } - - mineflayer.ticker.on('tick', () => { - mineflayer.status.update(mineflayer) - mineflayer.isCreative = mineflayer.bot.game?.gameMode === 'creative' - mineflayer.allowCheats = false - }) - - return mineflayer - } - - public async loadPlugin(plugin: MineflayerPlugin) { - if (plugin.created) - await plugin.created(this) - - if (plugin.loadPlugin) { - this.bot.loadPlugin(await plugin.loadPlugin(this, this.bot, this.options.botConfig)) - } - - if (plugin.spawned) - this.bot.once('spawn', () => plugin.spawned?.(this)) - } - - public onCommand(commandName: string, cb: EventsHandler<'command'>) { - this.commands.set(commandName, cb) - } - - public onTick(event: TickEvents, cb: TickEventsHandler) { - this.ticker.on(event, cb) - } - - public async stop() { - for (const plugin of this.options?.plugins || []) { - if (plugin.beforeCleanup) { - await plugin.beforeCleanup(this) - } - } - this.components.cleanup() - this.bot.removeListener('chat', this.handleCommand()) - this.bot.end() - this.removeAllListeners() - } - - private handleCommand() { - return formBotChat(this.username, (sender, message) => { - const { isCommand, command, args } = parseCommand(sender, message) - - if (!isCommand) - return - - // Remove the # prefix from command - const cleanCommand = command.slice(1) - this.logger.withFields({ sender, command: cleanCommand, args }).log('Command received') - - const handler = this.commands.get(cleanCommand) - if (handler) { - handler({ time: this.bot.time.timeOfDay, command: { sender, isCommand, command: cleanCommand, args } }) - return - } - - // Built-in commands - switch (cleanCommand) { - case 'help': { - const commandList = Array.from(this.commands.keys()).concat(['help']) - this.bot.chat(`Available commands: ${commandList.map(cmd => `#${cmd}`).join(', ')}`) - break - } - default: - this.bot.chat(`Unknown command: ${cleanCommand}`) - } - }) - } - - private async handleMessage(_source: string, _message: string, _maxResponses: number = Infinity) { - // if (!source || !message) { - // console.warn('Received empty message from', source); - // return false; - // } - - // let used_command = false; - // if (maxResponses === null) { - // maxResponses = settings.max_commands === -1 ? Infinity : settings.max_commands; - // } - // if (maxResponses === -1) { - // maxResponses = Infinity; - // } - - // const self_prompt = source === 'system' || source === ctx.botName; - // const from_other_bot = convoManager.isOtherAgent(source); - - // if (!self_prompt && !from_other_bot) { // from user, check for forced commands - // const user_command_name = containsCommand(message); - // if (user_command_name) { - // if (!commandExists(user_command_name)) { - // this.routeResponse(source, `Command '${user_command_name}' does not exist.`); - // return false; - // } - // this.routeResponse(source, `*${source} used ${user_command_name.substring(1)}*`); - // if (user_command_name === '!newAction') { - // // all user-initiated commands are ignored by the bot except for this one - // // add the preceding message to the history to give context for newAction - // this.history.add(source, message); - // } - // let execute_res = await executeCommand(this, message); - // if (execute_res) - // this.routeResponse(source, execute_res); - // return true; - // } - // } - - // if (from_other_bot) - // this.last_sender = source; - - // // Now translate the message - // message = await handleEnglishTranslation(message); - // console.log('received message from', source, ':', message); - - // const checkInterrupt = () => this.self_prompter.shouldInterrupt(self_prompt) || this.shut_up || convoManager.responseScheduledFor(source); - - // let behavior_log = this.bot.modes.flushBehaviorLog(); - // if (behavior_log.trim().length > 0) { - // const MAX_LOG = 500; - // if (behavior_log.length > MAX_LOG) { - // behavior_log = '...' + behavior_log.substring(behavior_log.length - MAX_LOG); - // } - // behavior_log = 'Recent behaviors log: \n' + behavior_log.substring(behavior_log.indexOf('\n')); - // await this.history.add('system', behavior_log); - // } - - // // Handle other user messages - // await this.history.add(source, message); - // this.history.save(); - - // if (!self_prompt && this.self_prompter.on) // message is from user during self-prompting - // maxResponses = 1; // force only respond to this message, then let self-prompting take over - // for (let i=0; i 0) - // chat_message = `${pre_message} ${chat_message}`; - // this.routeResponse(source, chat_message); - // } - - // let execute_res = await executeCommand(this, res); - - // console.log('Agent executed:', command_name, 'and got:', execute_res); - // used_command = true; - - // if (execute_res) - // this.history.add('system', execute_res); - // else - // break; - // } - // else { // conversation response - // this.history.add(this.name, res); - // this.routeResponse(source, res); - // break; - // } - - // this.history.save(); - // } - - // return used_command; - } -} +export * from './action' +export * from './command' +export * from './components' +export * from './core' +export * from './health' +export * from './interfaces' +export * from './memory' +export * from './message' +export * from './plugin' +export * from './status' +export * from './ticker' +export * from './types' diff --git a/services/minecraft/src/libs/mineflayer/interfaces.ts b/services/minecraft/src/libs/mineflayer/interfaces.ts new file mode 100644 index 000000000..b55ec74d1 --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/interfaces.ts @@ -0,0 +1,3 @@ +export interface OneLinerable { + toOneLiner: () => string +} diff --git a/services/minecraft/src/libs/mineflayer/memory.ts b/services/minecraft/src/libs/mineflayer/memory.ts new file mode 100644 index 000000000..25fb96331 --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/memory.ts @@ -0,0 +1,12 @@ +import type { Message } from 'neuri/openai' +import type { Action } from './action' + +export class Memory { + public chatHistory: Message[] + public actions: Action[] + + constructor() { + this.chatHistory = [] + this.actions = [] + } +} diff --git a/services/minecraft/src/libs/mineflayer/status.ts b/services/minecraft/src/libs/mineflayer/status.ts new file mode 100644 index 000000000..6b5c01ae6 --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/status.ts @@ -0,0 +1,46 @@ +import type { Mineflayer } from './core' +import type { OneLinerable } from './interfaces' + +export class Status implements OneLinerable { + public position: string + public health: string + public weather: string + public timeOfDay: string + + constructor() { + this.position = '' + this.health = '' + this.weather = '' + this.timeOfDay = '' + } + + public update(mineflayer: Mineflayer) { + if (!mineflayer.ready) + return + + Object.assign(this, Status.from(mineflayer)) + } + + static from(mineflayer: Mineflayer): Status { + if (!mineflayer.ready) + return new Status() + + const pos = mineflayer.bot.entity.position + const weather = mineflayer.bot.isRaining ? 'Rain' : mineflayer.bot.thunderState ? 'Thunderstorm' : 'Clear' + const timeOfDay = mineflayer.bot.time.timeOfDay < 6000 + ? 'Morning' + : mineflayer.bot.time.timeOfDay < 12000 ? 'Afternoon' : 'Night' + + const status = new Status() + status.position = `x: ${pos.x.toFixed(2)}, y: ${pos.y.toFixed(2)}, z: ${pos.z.toFixed(2)}` + status.health = `${Math.round(mineflayer.bot.health)} / 20` + status.weather = weather + status.timeOfDay = timeOfDay + + return status + } + + public toOneLiner(): string { + return Object.entries(this).map(([key, value]) => `${key}: ${value}`).join('\n') + } +} diff --git a/services/minecraft/src/libs/mineflayer/types.ts b/services/minecraft/src/libs/mineflayer/types.ts new file mode 100644 index 000000000..861b5a62b --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/types.ts @@ -0,0 +1,19 @@ +import type { CommandContext } from './command' + +export interface Context { + time: number + command?: CommandContext +} + +export interface EventHandlers { + 'interrupt': () => void + 'command': (ctx: Context) => void | Promise + 'time:sunrise': (ctx: Context) => void + 'time:noon': (ctx: Context) => void + 'time:sunset': (ctx: Context) => void + 'time:midnight': (ctx: Context) => void +} + +export type Events = keyof EventHandlers +export type EventsHandler = EventHandlers[K] +export type Handler = (ctx: Context) => void | Promise