874 lines
23 KiB
JavaScript
874 lines
23 KiB
JavaScript
'use strict';
|
|
|
|
const mineflayer = require('mineflayer');
|
|
const minecraftData = require('minecraft-data');
|
|
const { pathfinder, Movements, goals: { GoalNear } } = require('mineflayer-pathfinder');
|
|
const Vec3 = require('vec3');
|
|
const {sleep} = require('../utils');
|
|
|
|
|
|
class CJbot{
|
|
isReady = false;
|
|
listeners = {};
|
|
|
|
// Holds the minimum cool down time for chat messages
|
|
nextChatTime = 1500;
|
|
|
|
// Prevents executing commands while one is running
|
|
commandLock = false;
|
|
commandCollDownTime = 1500;
|
|
|
|
// Holds the list of commands this bot can execute
|
|
commands = {};
|
|
|
|
|
|
combatTag = false;
|
|
doLogOut = false;
|
|
|
|
// Map of all the bots currently in use
|
|
static bots = {};
|
|
|
|
// Forces the code to wait while until the bot is loaded before moving on to
|
|
// the next
|
|
static __addLock = false;
|
|
|
|
// Holds a list of bots that still needs to be connect
|
|
static __toConnect = [];
|
|
|
|
|
|
constructor(args){
|
|
|
|
// Friendly name to access the bot in `CJbot.bots`
|
|
this.name = args.name || args.username;
|
|
this.username = args.username;
|
|
this.password = args.password;
|
|
this.host = args.host;
|
|
this.auth = args.auth || 'microsoft';
|
|
this.version = args.version || '1.20.1';
|
|
this.hasAi = args.hasAi;
|
|
|
|
//
|
|
this.pluginsWanted = args.plugins || {};
|
|
|
|
// States if the bot should connect when its loaded
|
|
this.autoReConnect = 'autoConnect' in args ? args.autoReConnect : true;
|
|
this.autoConnect = 'autoConnect' in args ? args.autoConnect : true;
|
|
|
|
// On-demand mode: bot stays offline until ensureConnected() is called
|
|
this.onDemand = args.onDemand || false;
|
|
this._idleTimer = null;
|
|
this._taskQueue = [];
|
|
this._connecting = false;
|
|
this._idleTimeout = args.idleTimeout || 30000;
|
|
|
|
// If we want the be always connected, kick off the function to auto
|
|
// reconnect
|
|
if(this.autoReConnect && !this.onDemand) this.__autoReConnect()
|
|
}
|
|
|
|
connect(){
|
|
console.log('CJbot.connect');
|
|
return new Promise((resolve, reject) =>{
|
|
|
|
try{
|
|
this.bot = mineflayer.createBot({
|
|
host: this.host,
|
|
username: this.username,
|
|
password: this.password,
|
|
version: this.version,
|
|
auth: this.auth,
|
|
});
|
|
|
|
// If an error happens before the login event, toss an error back to
|
|
// the caller of the function
|
|
let onError = this.bot.on('error', (m)=>{
|
|
console.log('ERROR CJbot.connect on error:', this.name, m.toString());
|
|
reject(m);
|
|
})
|
|
|
|
// If the connection ends before the login event, toss an error back
|
|
// to the caller of the function
|
|
this.bot.on('end', (reason, ...args)=>{
|
|
console.log(this.name, 'Connection ended:', reason, ...args);
|
|
this.pluginUnloadAll(this.onDemand); // keepDb=true for on-demand
|
|
this.isReady = false;
|
|
reject(reason);
|
|
});
|
|
|
|
// When the bot is ready, return to the caller success
|
|
this.bot.on('spawn', async()=>{
|
|
console.log('CJbot.connect on spawn')
|
|
await sleep(2000);
|
|
this.__onReady();
|
|
resolve();
|
|
this._pluginsReady = this.pluginLoadAll();
|
|
});
|
|
|
|
}catch(error){
|
|
console.log('CJbot.connect Error', error);
|
|
reject(error);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
// Wrap the once method so it works correctly with await
|
|
once(event){
|
|
return new Promise((resolve, reject)=> this.bot.once(event, resolve));
|
|
}
|
|
|
|
// Internal method to kick off the functionality after its loaded in the
|
|
// server
|
|
async __onReady(){try{
|
|
|
|
this.bot.loadPlugin(pathfinder);
|
|
this.mcData = minecraftData(this.bot.version);
|
|
this.defaultMove = new Movements(this.bot, this.mcData);
|
|
this.defaultMove.canDig = false;
|
|
this.defaultMove.allowSprinting = false;
|
|
|
|
// Make pathfinder avoid routing through chests/shulkers
|
|
if (!this.defaultMove.blocksCost) this.defaultMove.blocksCost = {};
|
|
const avoidBlocks = ['chest', 'trapped_chest', 'ender_chest',
|
|
'shulker_box', 'white_shulker_box', 'orange_shulker_box',
|
|
'magenta_shulker_box', 'light_blue_shulker_box', 'yellow_shulker_box',
|
|
'lime_shulker_box', 'pink_shulker_box', 'gray_shulker_box',
|
|
'light_gray_shulker_box', 'cyan_shulker_box', 'purple_shulker_box',
|
|
'blue_shulker_box', 'brown_shulker_box', 'green_shulker_box',
|
|
'red_shulker_box', 'black_shulker_box'];
|
|
for (const name of avoidBlocks) {
|
|
const block = this.mcData.blocksByName[name];
|
|
if (block) this.defaultMove.blocksCost[block.id] = 100;
|
|
}
|
|
|
|
this.bot.pathfinder.setMovements(this.defaultMove);
|
|
this._setupAntiStuck();
|
|
|
|
// Add the listeners to the bot. We do this so if the bot loses
|
|
// connection, the mineflayer instance will also be lost.
|
|
this.isReady = true;
|
|
|
|
this.__error();
|
|
this.__startListeners();
|
|
|
|
// Call the internal listeners when the bot is ready
|
|
for(let callback of this.listeners.onReady || []){
|
|
callback.call(this);
|
|
}
|
|
|
|
console.log('Bot is ready', this.bot.entity.username, this.username);
|
|
|
|
// Start chat listeners
|
|
this.__listen();
|
|
|
|
this.bot.on('title', (...args)=>console.log('on title', args))
|
|
|
|
|
|
}catch(error){
|
|
console.error('minecraft.js | __onReady | ', this.name, ' ', error);
|
|
}}
|
|
|
|
__startListeners(){
|
|
for(let event in this.listeners){
|
|
console.log('__adding listeners', event)
|
|
for(let callback of this.listeners[event]){
|
|
this.bot.on(event, callback);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wrap the .on method so we can hold the listeners internally
|
|
on(event, callback){
|
|
if(!this.listeners[event]) this.listeners[event] = [];
|
|
|
|
this.listeners[event].push(callback);
|
|
|
|
// If the bot is already loaded, add the passed listener to the
|
|
// mineflayer instance
|
|
if(this.isReady){
|
|
if(event === 'onReady') callback(this);
|
|
else this.bot.on(event, callback);
|
|
}
|
|
|
|
return ()=> this.off(event, callback);
|
|
}
|
|
|
|
// Remove listener for events
|
|
off(event, callback) {
|
|
console.log('off', event, callback)
|
|
if (!this.listeners[event]) return false;
|
|
|
|
const index = this.listeners[event].indexOf(callback);
|
|
if (index === -1) return false;
|
|
|
|
this.listeners[event].splice(index, 1);
|
|
|
|
// If bot is ready, also remove from the Mineflayer bot
|
|
if (this.isReady) {
|
|
this.bot.off(event, callback);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Listen for ending events and call connect again
|
|
__autoReConnect(){
|
|
try{
|
|
console.log('auto re-connect function')
|
|
this.on('kicked', (...args)=>console.log('CJbot.__autoReConnect on kick', args))
|
|
|
|
this.on('end', async (...args)=>{
|
|
console.error('CJbot.__autoReConnect on end', args)
|
|
|
|
await sleep(30000)
|
|
this.connect()
|
|
});
|
|
|
|
|
|
this.on('error', (error)=>{
|
|
console.error('MC on error', error);
|
|
|
|
// this.connect();
|
|
});
|
|
}catch(error){
|
|
console.error('error in __autoReConnect', error);
|
|
}
|
|
}
|
|
|
|
__error(message){
|
|
this.bot.on('error', (error)=>{
|
|
console.error(`ERROR!!! MC bot ${this.username} on ${this.host} had an error:\n`, error)
|
|
});
|
|
}
|
|
|
|
|
|
quit(force){
|
|
if(!this.combatTag || force){
|
|
this.bot.quit();
|
|
}else{
|
|
console.log('Logout prevented due to combatTag');
|
|
this.doLogOut = true;
|
|
}
|
|
}
|
|
|
|
/* Plugins */
|
|
|
|
static plungins = {};
|
|
|
|
static pluginAdd(cls){
|
|
this.plungins[cls.name] = cls;
|
|
// Queue for web registration if plugin has web support
|
|
if (typeof cls.createRouter === 'function' || cls.webUI) {
|
|
const webServer = require('../controller/web-server');
|
|
webServer.queuePlugin(cls);
|
|
}
|
|
}
|
|
|
|
plunginsLoaded = {};
|
|
|
|
async pluginLoadAll(){
|
|
for(let pluginName in this.pluginsWanted){
|
|
await this.pluginLoad(pluginName, this.pluginsWanted[pluginName]);
|
|
}
|
|
}
|
|
|
|
async pluginLoad(pluginName, opts){
|
|
console.log('CJbot.pluginLoad', pluginName)
|
|
let plugin = new this.constructor.plungins[pluginName]({...opts, bot:this})
|
|
await plugin.init();
|
|
this.plunginsLoaded[pluginName] = plugin;
|
|
}
|
|
|
|
async pluginUnload(name){
|
|
console.log('CJbot.pluginUnload', name)
|
|
if(this.plunginsLoaded[name]){
|
|
this.plunginsLoaded[name].unload();
|
|
delete this.plunginsLoaded[name];
|
|
console.log('CJbot.pluginUnload', name, 'done');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
async pluginUnloadAll(keepDb = false){
|
|
console.log('CJbot.pluginUnloadAll', keepDb ? '(keepDb)' : '');
|
|
for(let pluginName in this.plunginsLoaded){
|
|
console.log('CJbot.pluginUnloadAll loop', pluginName)
|
|
try{
|
|
await this.plunginsLoaded[pluginName].unload(keepDb);
|
|
delete this.plunginsLoaded[pluginName];
|
|
}catch(error){
|
|
console.log('CJbot.pluginUnload loop error:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/* On-demand lifecycle */
|
|
|
|
async ensureConnected(taskFn) {
|
|
this._resetIdleTimer();
|
|
if (this.isReady) return await taskFn();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this._taskQueue.push({ fn: taskFn, resolve, reject });
|
|
if (this._connecting) return;
|
|
|
|
this._connecting = true;
|
|
this.connect().then(async () => {
|
|
this._connecting = false;
|
|
// Wait for plugins to finish loading (pluginLoadAll runs after spawn)
|
|
await this._pluginsReady;
|
|
await this._drainTaskQueue();
|
|
}).catch((error) => {
|
|
this._connecting = false;
|
|
const queue = this._taskQueue.splice(0);
|
|
for (const task of queue) task.reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
async _drainTaskQueue() {
|
|
while (this._taskQueue.length > 0) {
|
|
const task = this._taskQueue.shift();
|
|
try {
|
|
task.resolve(await task.fn());
|
|
} catch (error) {
|
|
task.reject(error);
|
|
}
|
|
this._resetIdleTimer();
|
|
}
|
|
}
|
|
|
|
_resetIdleTimer() {
|
|
if (!this.onDemand) return;
|
|
if (this._idleTimer) clearTimeout(this._idleTimer);
|
|
this._idleTimer = setTimeout(() => this._idleDisconnect(), this._idleTimeout);
|
|
}
|
|
|
|
_idleDisconnect() {
|
|
if (!this.isReady) return;
|
|
console.log(`${this.name}: Idle timeout, disconnecting on-demand bot`);
|
|
this.quit(true);
|
|
}
|
|
|
|
/* Chat and messaging*/
|
|
|
|
__listen(){
|
|
this.bot.on('chat', (from, message)=>{
|
|
|
|
// Ignore messages from this bot
|
|
if(from === this.bot.entity.username) return;
|
|
|
|
// Filter messages to this bot
|
|
if(message.startsWith(this.bot.entity.username)){
|
|
this.__doCommand(
|
|
from,
|
|
message.replace(`${this.bot.entity.username} ` , ' ').trim()
|
|
)
|
|
}
|
|
});
|
|
|
|
this.bot.on('whisper', (from, message)=>{
|
|
this.__doCommand(
|
|
from,
|
|
message.replace(`${this.bot.entity.username} ` , ' ').trim()
|
|
)
|
|
});
|
|
|
|
this.bot.on('message', (message, type)=>{
|
|
if(message.toString().includes(' invited you to teleport to him.')){
|
|
// teleport invite
|
|
|
|
console.log('found teleport', message.toString().split(' ')[0])
|
|
this.__doCommand(message.toString().split(' ')[0], '.invite');
|
|
}
|
|
|
|
if(message.toString().includes(' wants to trade with you!')){
|
|
// teleport invite
|
|
|
|
console.log('found Trade', message.toString().split(' ')[0])
|
|
this.__doCommand(message.toString().split(' ')[0], '.trade');
|
|
}
|
|
|
|
|
|
if(message.toString().includes('You are combat tagged by')){
|
|
try{
|
|
this.combatTag = true;
|
|
console.log('was attacked by')
|
|
let attacker = message.toString().split('. ')[0].replace('You are combat tagged by ', '')
|
|
console.log('was attacked by', attacker)
|
|
// teleport invite
|
|
this.whisper(attacker, 'Please do not attack me, I am a bot.')
|
|
|
|
}catch(error){
|
|
console.log('error!!!!!!', error)
|
|
}
|
|
|
|
}
|
|
|
|
if(message.toString().includes('You are no longer in combat. You may now logout.')){
|
|
this.combatTag = false
|
|
if(this.doLogOut){
|
|
this.quit()
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
__chatCoolDown(){
|
|
return Math.floor(Math.random() * (3000 - 2000) + 2000);
|
|
}
|
|
|
|
async say(...messages){
|
|
for(let message of messages){
|
|
(async (message)=>{
|
|
if(this.nextChatTime > Date.now()){
|
|
await sleep(this.nextChatTime-Date.now()+1)
|
|
}
|
|
this.bot.chat(message);
|
|
})(message);
|
|
|
|
this.nextChatTime = Date.now() + this.__chatCoolDown();
|
|
}
|
|
}
|
|
|
|
async sayAiSafe(...messages){
|
|
for(let message of messages){
|
|
if(message.startsWith('/') && !(message.startsWith('/msg') || message.startsWith('/help'))){
|
|
console.log('bot tried to execute bad command', message);
|
|
message = '.'+message;
|
|
}
|
|
await this.say(message);
|
|
}
|
|
}
|
|
|
|
async whisper(to, ...messages){
|
|
await this.say(...messages.map(message=>`/msg ${to} ${message}`));
|
|
}
|
|
|
|
/* Commands */
|
|
|
|
async __unLockCommand(time){
|
|
await sleep(this.commandCollDownTime);
|
|
this.commandLock = false;
|
|
}
|
|
|
|
__reduceCommands(from){
|
|
return Object.keys(this.commands).filter(command =>{
|
|
if (this.commands[command].allowed && !this.commands[command].allowed.includes(from)) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
async __doCommand(from, command){try{
|
|
let [cmd, ...parts] = command.split(/\s+/);
|
|
|
|
if(!this.__reduceCommands(from).includes(cmd)) return;
|
|
|
|
const cmdDef = this.commands[cmd];
|
|
|
|
if(this.commandLock && !cmdDef.ignoreLock){
|
|
this.whisper(from, `cool down, try again in ${this.commandCollDownTime/1000} seconds...`);
|
|
return ;
|
|
}
|
|
|
|
if(!cmdDef.ignoreLock) this.commandLock = true;
|
|
try{
|
|
await cmdDef.function.call(this, from, ...parts);
|
|
}catch(error){
|
|
this.whisper(from, `The command encountered an error.`);
|
|
this.whisper(from, `ERROR: ${error}`);
|
|
console.error(`Chat command error on ${cmd} from ${from}\n`, error);
|
|
}
|
|
if(!cmdDef.ignoreLock) this.__unLockCommand();
|
|
}catch(error){
|
|
console.error('minecraft.js | __doCommand |', this.name, ' ', error)
|
|
}}
|
|
|
|
addCommand(name, obj){
|
|
if(this.commands[name]) return false;
|
|
|
|
this.commands[name] = obj;
|
|
}
|
|
|
|
getPlayers(){
|
|
for (let [username, value] of Object.entries(this.bot.players)){
|
|
value.lvl = Number(value.displayName.extra[0].text)
|
|
}
|
|
|
|
return this.bot.players;
|
|
}
|
|
|
|
/* Actions */
|
|
|
|
__blockOrVec(thing){
|
|
if(thing instanceof Vec3.Vec3) return this.bot.blockAt(thing);
|
|
if(thing.constructor && thing.constructor.name === 'Block') return thing;
|
|
|
|
throw new Error('Not supported block identifier');
|
|
}
|
|
|
|
findBlockBySign(text){
|
|
return this.bot.findBlock({
|
|
useExtraInfo: true,
|
|
maxDistance: 64,
|
|
matching: (block)=> {
|
|
if(block.name.includes('sign') && block.signText.includes(text)){
|
|
return true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
findChestBySign(text){
|
|
return this.bot.findBlock({
|
|
point: this.findBlockBySign(text).position,
|
|
// maxDistance: 1,
|
|
useExtraInfo: true,
|
|
matching: block => block.name === 'chest'
|
|
});
|
|
}
|
|
|
|
isWithinRange(target, range=2){
|
|
const botPos = this.bot.entity.position;
|
|
const distance = botPos.distanceTo(target);
|
|
|
|
return distance <= range+.9;
|
|
}
|
|
|
|
playerWithinBlock(player, block, range){
|
|
let playerData = this.bot.players[player];
|
|
if(!playerData || !playerData.entity) return; // Skip if no entity info
|
|
|
|
// Calculate the distance between the player and the block
|
|
let distance = playerData.entity.position.distanceTo(block.position);
|
|
|
|
console.log('CJbot.playerWithinBlock', distance, range, distance < range)
|
|
if(!range){
|
|
return distance;
|
|
}
|
|
|
|
return distance < range;
|
|
}
|
|
|
|
// Global anti-stuck system: monitors every physics tick while pathfinder is
|
|
// moving. If the bot hasn't moved for ~600ms (12 ticks), it stops the
|
|
// pathfinder and nudges the bot in an alternating direction (back/left/right)
|
|
// to free it from corners. This works for ALL pathfinder movement globally.
|
|
_antiStuckNudging = false;
|
|
|
|
_setupAntiStuck() {
|
|
let lastPos = null;
|
|
let stuckTicks = 0;
|
|
let nudgeTicks = 0;
|
|
let nudgeCount = 0;
|
|
let idleTicks = 0;
|
|
|
|
this.bot.on('physicsTick', () => {
|
|
// During a nudge, count ticks then clear controls
|
|
if (this._antiStuckNudging) {
|
|
nudgeTicks++;
|
|
if (nudgeTicks >= 8) { // ~400ms of nudge movement
|
|
this.bot.clearControlStates();
|
|
this._antiStuckNudging = false;
|
|
nudgeTicks = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Track position regardless of pathfinder state — goTo clears
|
|
// the goal between retries which would reset our counter
|
|
const pos = this.bot.entity.position;
|
|
|
|
if (!this.bot.pathfinder.isMoving()) {
|
|
// Not pathfinding — only reset if we've been idle a while
|
|
// (short gaps between goTo retries shouldn't reset)
|
|
idleTicks++;
|
|
if (idleTicks > 40) { // ~2 seconds of no pathfinding = truly idle
|
|
stuckTicks = 0;
|
|
lastPos = null;
|
|
nudgeCount = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
idleTicks = 0;
|
|
|
|
if (!lastPos) {
|
|
lastPos = pos.clone();
|
|
return;
|
|
}
|
|
|
|
if (lastPos.distanceTo(pos) < 0.05) {
|
|
stuckTicks++;
|
|
|
|
if (stuckTicks >= 12) { // ~600ms with no movement
|
|
nudgeCount++;
|
|
const directions = ['back', 'left', 'right'];
|
|
const dir = directions[nudgeCount % 3];
|
|
console.log(`AntiStuck: No movement for ${stuckTicks} ticks, nudging ${dir}`);
|
|
|
|
// Stop pathfinder so our controls take effect
|
|
this.bot.pathfinder.stop();
|
|
|
|
// Apply nudge direction
|
|
this.bot.setControlState(dir, true);
|
|
this._antiStuckNudging = true;
|
|
nudgeTicks = 0;
|
|
stuckTicks = 0;
|
|
lastPos = null;
|
|
}
|
|
} else {
|
|
stuckTicks = 0;
|
|
lastPos = pos.clone();
|
|
}
|
|
});
|
|
}
|
|
|
|
async goTo(options) {
|
|
let range = options.range || 2;
|
|
let block = this.__blockOrVec(options.where);
|
|
let retries = 0;
|
|
let noPathCount = 0;
|
|
const maxRetries = options.maxRetries || 5;
|
|
const unjamDirections = ['back', 'left', 'right', 'forward'];
|
|
|
|
while(!this.isWithinRange(this.__blockOrVec(options.where).position, range)){
|
|
if(retries >= maxRetries){
|
|
// All pathfinder attempts failed — clear goal, relocate, retry fresh
|
|
console.log(`goTo: Pathfinder failed ${maxRetries} times, relocating and retrying`);
|
|
|
|
this.bot.pathfinder.setGoal(null);
|
|
await sleep(200);
|
|
|
|
// Walk backward and randomly strafe to a new position
|
|
const side = Math.random() < 0.5 ? 'left' : 'right';
|
|
this.bot.setControlState('back', true);
|
|
this.bot.setControlState(side, true);
|
|
await sleep(600);
|
|
this.bot.clearControlStates();
|
|
await sleep(500);
|
|
|
|
// Reset retries and try again
|
|
retries = 0;
|
|
continue;
|
|
}
|
|
|
|
try{
|
|
// Timeout pathfinder after 30 seconds to prevent infinite hangs
|
|
await Promise.race([
|
|
this.bot.pathfinder.goto(
|
|
new GoalNear(...block.position.toArray(), range)
|
|
),
|
|
new Promise((_, reject) =>
|
|
setTimeout(() => {
|
|
this.bot.pathfinder.stop();
|
|
reject(new Error('goTo: Pathfinder timed out after 30s'));
|
|
}, 30000)
|
|
)
|
|
]);
|
|
}catch(error){
|
|
retries++;
|
|
const msg = error.message || String(error);
|
|
const target = block.position;
|
|
console.log(`goTo: Attempt ${retries}/${maxRetries} to ${target} error: ${msg}`);
|
|
|
|
// "No path" = pathfinder searched full graph, route doesn't exist.
|
|
// Walking around won't help — skip this target after 2 attempts.
|
|
if(msg.includes('No path')){
|
|
noPathCount++;
|
|
if(noPathCount >= 2){
|
|
console.log(`goTo: No path to ${target} after ${noPathCount} attempts, skipping`);
|
|
this.bot.pathfinder.setGoal(null);
|
|
return false;
|
|
}
|
|
// One retry: increase range in case we're just barely blocked
|
|
range = Math.min(range + 1, 5);
|
|
console.log(`goTo: No path, increasing range to ${range} and retrying`);
|
|
this.bot.pathfinder.setGoal(null);
|
|
await sleep(500);
|
|
continue;
|
|
}
|
|
|
|
// Wait for any ongoing anti-stuck nudge to finish
|
|
while(this._antiStuckNudging) await sleep(100);
|
|
|
|
// Clear pathfinder state
|
|
this.bot.pathfinder.setGoal(null);
|
|
await sleep(200);
|
|
|
|
// Unjam: cycle through different directions each retry
|
|
const dir = unjamDirections[(retries - 1) % unjamDirections.length];
|
|
const side = Math.random() < 0.5 ? 'left' : 'right';
|
|
console.log(`goTo: Unjamming — walking ${dir} + ${side}`);
|
|
this.bot.setControlState(dir, true);
|
|
this.bot.setControlState(side, true);
|
|
await sleep(800);
|
|
this.bot.clearControlStates();
|
|
await sleep(300);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async goToReturn(options){
|
|
let here = this.bot.entity.position;
|
|
let hereYaw = this.bot.entity.yaw
|
|
await this.goTo(options);
|
|
|
|
return async () =>{
|
|
await this.goTo({where: here, range: 0}, true);
|
|
await sleep(500);
|
|
await this.bot.look(Math.floor(hereYaw), 0);
|
|
}
|
|
}
|
|
|
|
async __nextContainerSlot(window, item) {
|
|
let firstEmptySlot = false;
|
|
|
|
await window.containerItems();
|
|
|
|
for(let slot in window.slots.slice(0, window.inventoryStart)){
|
|
if(window.slots[slot] === null ){
|
|
if(!Number.isInteger(firstEmptySlot)) firstEmptySlot = Number(slot);
|
|
continue;
|
|
}
|
|
if(item.type === window.slots[slot].type && window.slots[slot].count < window.slots[slot].stackSize){
|
|
return slot;
|
|
}
|
|
}
|
|
|
|
return firstEmptySlot;
|
|
}
|
|
|
|
async openContainer(block){
|
|
let count = 0;
|
|
block = this.__blockOrVec(block);
|
|
let window;
|
|
|
|
while(!this.bot.currentWindow){
|
|
try{
|
|
window = await this.bot.openContainer(block);
|
|
}catch(error){
|
|
if(!error.message.includes('Event windowOpen did not fire within timeout')) throw error;
|
|
}
|
|
if(this.bot.currentWindow?.title){
|
|
break;
|
|
}
|
|
this.bot.removeAllListeners('windowOpen');
|
|
await sleep(1500);
|
|
|
|
if(count++ == 3) throw 'Block wont open';
|
|
}
|
|
|
|
return this.bot.currentWindow;
|
|
}
|
|
|
|
async openCraftingTable(block){
|
|
let count = 0;
|
|
block = this.__blockOrVec(block);
|
|
this.bot.activateBlock(block);
|
|
let window = await this.once('windowOpen');
|
|
|
|
return window;
|
|
}
|
|
|
|
async checkItemsFromContainer(containerBlock, itemName, count){
|
|
let currentSlot = 0;
|
|
let foundCount = 0;
|
|
let window = await this.openContainer(containerBlock);
|
|
|
|
for(let slot of window.slots){
|
|
if(currentSlot++ === window.inventoryStart) break;currentSlot
|
|
if(!slot) continue;
|
|
if(slot.name === itemName) foundCount += slot.count;
|
|
}
|
|
|
|
await this.bot.closeWindow(window);
|
|
if(foundCount >= count) return true;
|
|
}
|
|
|
|
async getItemsFromChest(containerBlock, itemName, count){
|
|
let window = await this.openContainer(containerBlock);
|
|
await sleep(500);
|
|
// console.log('item id', this.mcData.itemsByName[itemName], this.mcData)
|
|
await window.withdraw(this.mcData.itemsByName[itemName].id, null, count);
|
|
await this.bot.closeWindow(window);
|
|
}
|
|
|
|
async getFullShulkersFromChest(chestBlock, item) {
|
|
const fullShulkers = [];
|
|
|
|
let window = await this.openContainer(chestBlock);
|
|
|
|
let itemCount = 0
|
|
let currentSlot = 0;
|
|
for(let slot of window.slots){
|
|
if(currentSlot++ === window.inventoryStart) break;
|
|
|
|
if(!slot || slot.name !== 'shulker_box') continue;
|
|
// console.log('slot:', slot)
|
|
if(slot.nbt){
|
|
// console.log('nbt', slot.nbt, slot.nbt.value.BlockEntityTag)
|
|
// console.log('BlockEntityTag:', slot.nbt.value.BlockEntityTag.value.Items.value.value)
|
|
|
|
for(let shulkerSlot of slot.nbt.value.BlockEntityTag.value.Items.value.value){
|
|
console.log('shulkerSlot', shulkerSlot)
|
|
if(shulkerSlot.id?.value !== `minecraft:${item}`) continue;
|
|
itemCount += shulkerSlot.Count.value
|
|
}
|
|
if(this.bot.registry.itemsByName[item].stackSize * 27 === itemCount){
|
|
console.log('found full shulker');
|
|
this.bot.moveSlotItem(currentSlot, window.inventoryStart);
|
|
break;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
await window.close();
|
|
}
|
|
|
|
async dumpToChest(block, blockName, amount) {
|
|
|
|
let window = await this.openContainer(block);
|
|
|
|
let items = window.slots.slice(window.inventoryStart).filter(function(item){
|
|
if(!item) return false;
|
|
if(blockName && blockName !== item.name) return false;
|
|
return true;
|
|
});
|
|
|
|
for(let item of items){
|
|
await sleep(500);
|
|
let currentSlot = Number(item.slot);
|
|
if(!window.slots[currentSlot]) continue;
|
|
|
|
try{
|
|
await this.bot.transfer({
|
|
window,
|
|
itemType: this.mcData.itemsByName[item.name].id,
|
|
sourceStart: currentSlot,
|
|
sourceEnd: currentSlot+1,
|
|
destStart: 0,
|
|
destEnd: window.inventoryStart-1,
|
|
count: amount || item.count,
|
|
})
|
|
|
|
if(amount) amount -= item.count
|
|
}catch(error){
|
|
console.log('error?', item.count, error.message, error);
|
|
}
|
|
}
|
|
|
|
await sleep(1000);
|
|
await this.bot.closeWindow(window);
|
|
|
|
return amount ? amount : true;
|
|
}
|
|
}
|
|
|
|
module.exports = {CJbot};
|