forked from wmantly/mc-bot-town
fable
This commit is contained in:
+227
-45
@@ -71,7 +71,11 @@ class CJbot{
|
||||
this._taskQueue = [];
|
||||
this._connecting = false;
|
||||
this._idleTimeout = args.idleTimeout || 30000;
|
||||
this._goToLock = false;
|
||||
this._goToLock = false;
|
||||
|
||||
// Bumped by interruptTask; in-flight goTo calls compare against the
|
||||
// value they captured at start and bail out when it changes.
|
||||
this._interruptGen = 0;
|
||||
|
||||
// If we want the be always connected, kick off the function to auto
|
||||
// reconnect
|
||||
@@ -245,8 +249,24 @@ class CJbot{
|
||||
this.on('end', async (...args)=>{
|
||||
console.error('CJbot.__autoReConnect on end', args)
|
||||
|
||||
await sleep(30000)
|
||||
this.connect()
|
||||
// connect() also rejects on 'end', which used to surface as an
|
||||
// unhandled rejection (fatal on modern Node). Guard against
|
||||
// overlapping loops and retry until we get back in.
|
||||
if(this._reconnecting) return;
|
||||
this._reconnecting = true;
|
||||
try{
|
||||
while(true){
|
||||
await sleep(30000);
|
||||
try{
|
||||
await this.connect();
|
||||
break;
|
||||
}catch(error){
|
||||
console.error('CJbot.__autoReConnect retry failed:', this.name, error?.message || error);
|
||||
}
|
||||
}
|
||||
}finally{
|
||||
this._reconnecting = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -371,6 +391,13 @@ class CJbot{
|
||||
|
||||
_idleDisconnect() {
|
||||
if (!this.isReady) return;
|
||||
// A registered task (scan, organize, withdraw, trade) is still running —
|
||||
// disconnecting now would abandon shulkers and leak locks
|
||||
if (this._currentTask) {
|
||||
console.log(`${this.name}: Idle timer fired mid-task (${this._currentTask.task}), rescheduling`);
|
||||
this._resetIdleTimer();
|
||||
return;
|
||||
}
|
||||
console.log(`${this.name}: Idle timeout, disconnecting on-demand bot`);
|
||||
this.quit(true);
|
||||
}
|
||||
@@ -492,7 +519,7 @@ class CJbot{
|
||||
const cmdDef = this.commands[cmd];
|
||||
|
||||
if(this.commandLock && !cmdDef.ignoreLock){
|
||||
this.whisper(from, `cool down, try again in ${this.commandCollDownTime/1000} seconds...`);
|
||||
this.whisper(from, `I'm busy with another command right now, please try again shortly...`);
|
||||
return ;
|
||||
}
|
||||
|
||||
@@ -547,7 +574,7 @@ class CJbot{
|
||||
findChestBySign(text){
|
||||
return this.bot.findBlock({
|
||||
point: this.findBlockBySign(text).position,
|
||||
// maxDistance: 1,
|
||||
maxDistance: 4,
|
||||
useExtraInfo: true,
|
||||
matching: block => block.name === 'chest'
|
||||
});
|
||||
@@ -560,7 +587,7 @@ class CJbot{
|
||||
return distance <= range+.9;
|
||||
}
|
||||
|
||||
playerWithinBlock(player, block, range){
|
||||
playerWithinBlock(player, block, range){
|
||||
let playerData = this.bot.players[player];
|
||||
if(!playerData || !playerData.entity) return; // Skip if no entity info
|
||||
|
||||
@@ -575,43 +602,158 @@ playerWithinBlock(player, block, range){
|
||||
return distance < range;
|
||||
}
|
||||
|
||||
// Interrupt current movement so trade/storage commands can run
|
||||
// Interrupt current movement so trade/storage commands can run
|
||||
async interruptTask(from) {
|
||||
this._interrupted = true;
|
||||
this._interruptGen++;
|
||||
this._currentTask = null;
|
||||
try {
|
||||
this.bot.pathfinder.stop();
|
||||
} catch (e) { /* pathfinder may not be moving */ }
|
||||
this.bot.clearControlStates();
|
||||
}
|
||||
|
||||
registerTask(source, task, fn) {
|
||||
this._interrupted = false;
|
||||
this._currentTask = { source, task, fn };
|
||||
}
|
||||
|
||||
clearTask() {
|
||||
this._currentTask = null;
|
||||
}
|
||||
|
||||
wasInterrupted() {
|
||||
return this._interrupted;
|
||||
}
|
||||
|
||||
async goTo(options) {
|
||||
while (this._goToLock) await new Promise(r => setTimeout(r, 50));
|
||||
this._goToLock = true;
|
||||
try {
|
||||
let range = options.range || 2;
|
||||
let block = this.__blockOrVec(options.where);
|
||||
console.log('[goTo] moving to', block.position, 'range', range);
|
||||
while (this._goToLock) await new Promise(r => setTimeout(r, 50));
|
||||
this._goToLock = true;
|
||||
// Captured after acquiring the lock: an interrupt kills goTo calls that
|
||||
// were already running, not the interrupting command's own movement.
|
||||
const gen = this._interruptGen;
|
||||
try {
|
||||
let range = options.range || 2;
|
||||
let block = this.__blockOrVec(options.where);
|
||||
let timeout = options.timeout || 60000;
|
||||
const goal = block.position;
|
||||
console.log('[goTo] target:', goal.toArray(), 'range:', range);
|
||||
|
||||
while(!this.isWithinRange(block.position, range)){
|
||||
try{
|
||||
console.log('[goTo] loop: isMoving=', this.bot.pathfinder.isMoving(), 'inRange=', this.isWithinRange(block.position, range));
|
||||
if(this.bot.pathfinder.isMoving()){
|
||||
await sleep(500);
|
||||
continue;
|
||||
}
|
||||
await this.bot.pathfinder.goto(
|
||||
new GoalNear(...block.position.toArray(), range)
|
||||
);
|
||||
}catch(error){
|
||||
console.log('CJbot.goTo while loop error:', error)
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
const startTime = Date.now();
|
||||
let lastPos = this.bot.entity.position.clone();
|
||||
let stuckTime = 0;
|
||||
let recoveryCount = 0;
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
this._goToLock = false;
|
||||
}
|
||||
}
|
||||
// Fire-and-forget: starts pathfinder, never awaited.
|
||||
// PathStopped/GoalChanged/NoPath are expected during recovery.
|
||||
const startPathfinder = () => {
|
||||
this.bot.pathfinder.goto(
|
||||
new GoalNear(...goal.toArray(), range)
|
||||
).catch(e => {
|
||||
if (e.name !== 'PathStopped' && e.name !== 'GoalChanged' && e.name !== 'NoPath')
|
||||
console.log('[goTo] goto rejected:', e.name, e.message);
|
||||
});
|
||||
};
|
||||
|
||||
startPathfinder();
|
||||
|
||||
while (!this.isWithinRange(goal, range)) {
|
||||
if (this._interruptGen !== gen) {
|
||||
this.bot.pathfinder.stop();
|
||||
console.log('[goTo] interrupted');
|
||||
return false;
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
if (elapsed > timeout) {
|
||||
this.bot.pathfinder.stop();
|
||||
console.log('[goTo] timed out after', timeout, 'ms');
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentPos = this.bot.entity.position;
|
||||
const moved = currentPos.distanceTo(lastPos);
|
||||
|
||||
if (moved > 0.2) {
|
||||
// Making progress
|
||||
stuckTime = 0;
|
||||
recoveryCount = Math.max(0, recoveryCount - 1);
|
||||
lastPos = currentPos.clone();
|
||||
} else {
|
||||
// Not moving
|
||||
stuckTime += 400;
|
||||
}
|
||||
|
||||
// Pathfinder legitimately pauses during jumps, replans, and corner
|
||||
// turns — only treat 2s+ of zero movement as actually stuck.
|
||||
if (stuckTime >= 2000) {
|
||||
console.log('[goTo] stuck, recovery=', recoveryCount);
|
||||
await this._recoverFromStuck(recoveryCount);
|
||||
stuckTime = 0;
|
||||
recoveryCount++;
|
||||
lastPos = this.bot.entity.position.clone();
|
||||
if (recoveryCount > 8) {
|
||||
console.log('[goTo] giving up after', recoveryCount, 'recoveries');
|
||||
return false;
|
||||
}
|
||||
startPathfinder();
|
||||
}
|
||||
|
||||
if (!this.bot.pathfinder.isMoving()) {
|
||||
startPathfinder();
|
||||
}
|
||||
await sleep(400);
|
||||
}
|
||||
|
||||
this.bot.pathfinder.stop();
|
||||
console.log('[goTo] arrived');
|
||||
return true;
|
||||
} finally {
|
||||
this._goToLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Like goTo, but failure to arrive aborts the caller instead of letting it
|
||||
// operate on a container it never reached.
|
||||
async goToMust(options) {
|
||||
const arrived = await this.goTo(options);
|
||||
if (!arrived) {
|
||||
const pos = options.where?.position || options.where;
|
||||
throw new Error(`Could not reach ${pos} (interrupted, stuck, or timed out)`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Simple unstuck: back up N blocks, strafe N blocks (alternating left/right),
|
||||
// then let the main loop retry pathfinding to the goal.
|
||||
async _recoverFromStuck(failureCount) {
|
||||
this.bot.pathfinder.stop();
|
||||
this.bot.clearControlStates();
|
||||
await sleep(50);
|
||||
|
||||
// Cap at 3 blocks — backing way up just walks the bot away from the
|
||||
// goal and into other obstacles. No jump: jumping while reversing
|
||||
// climbs the bot onto chests and wedges it into corners.
|
||||
const n = Math.min(failureCount + 1, 3);
|
||||
const strafeDir = failureCount % 2 === 0 ? 'left' : 'right';
|
||||
const blockTime = 250; // ms per block (walking speed ~4.3 blocks/sec)
|
||||
|
||||
console.log('[goTo] recover: back', n, 'strafe', n, strafeDir);
|
||||
|
||||
// Back up N blocks
|
||||
this.bot.setControlState('back', true);
|
||||
await sleep(n * blockTime);
|
||||
this.bot.setControlState('back', false);
|
||||
await sleep(50);
|
||||
|
||||
// Strafe N blocks (alternating direction)
|
||||
this.bot.setControlState(strafeDir, true);
|
||||
await sleep(n * blockTime);
|
||||
this.bot.setControlState(strafeDir, false);
|
||||
|
||||
this.bot.clearControlStates();
|
||||
await sleep(100);
|
||||
}
|
||||
async goToReturn(options){
|
||||
let here = this.bot.entity.position;
|
||||
let hereYaw = this.bot.entity.yaw
|
||||
@@ -643,26 +785,66 @@ playerWithinBlock(player, block, range){
|
||||
}
|
||||
|
||||
async openContainer(block){
|
||||
let count = 0;
|
||||
block = this.__blockOrVec(block);
|
||||
let window;
|
||||
|
||||
while(!this.bot.currentWindow){
|
||||
// A window left open by a failed earlier operation would otherwise be
|
||||
// returned as-is below — for the wrong container, with wrong slot
|
||||
// indexes. Close it and start clean.
|
||||
if(this.bot.currentWindow){
|
||||
console.log('CJbot.openContainer: closing stale window', this.bot.currentWindow.title);
|
||||
try{
|
||||
window = await this.bot.openContainer(block);
|
||||
this.bot.closeWindow(this.bot.currentWindow);
|
||||
}catch(error){
|
||||
console.log('CJbot.openContainer: stale window close failed:', error.message);
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
// A chest with a solid block on top cannot open. If the obstruction is
|
||||
// a shulker box it's one of ours, abandoned by a failed cycle — break
|
||||
// it and try to collect it. Anything else is a hard, described failure
|
||||
// instead of four pointless retries.
|
||||
if(block.name.includes('chest')){
|
||||
const above = this.bot.blockAt(block.position.offset(0, 1, 0));
|
||||
if(above && above.boundingBox === 'block'){
|
||||
if(above.name.includes('shulker_box')){
|
||||
console.log(`CJbot.openContainer: chest at ${block.position} blocked by ${above.name} on top — breaking it`);
|
||||
try{
|
||||
await this.bot.lookAt(above.position.offset(0.5, 0.5, 0.5), true);
|
||||
await this.bot.dig(above, 'raycast');
|
||||
await sleep(300);
|
||||
// Try to catch the drop so the box isn't lost to despawn
|
||||
await this.goTo({ where: above.position, range: 0, timeout: 10000 });
|
||||
await sleep(500);
|
||||
}catch(error){
|
||||
throw new Error(`Chest at ${block.position} blocked by ${above.name} on top (recovery failed: ${error.message})`);
|
||||
}
|
||||
}else{
|
||||
throw new Error(`Chest at ${block.position} won't open: blocked by ${above.name} on top`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
while(true){
|
||||
// Face the container — some anti-cheat setups reject interactions
|
||||
// the player isn't looking at
|
||||
try{
|
||||
await this.bot.lookAt(block.position.offset(0.5, 0.5, 0.5), true);
|
||||
}catch(error){ /* best effort */ }
|
||||
|
||||
try{
|
||||
const window = await this.bot.openContainer(block);
|
||||
if(window) return window;
|
||||
}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');
|
||||
// The open packet may have landed even though the event timed out
|
||||
if(this.bot.currentWindow?.title) return this.bot.currentWindow;
|
||||
|
||||
if(++count > 3) throw new Error(`Block wont open (${block.name} at ${block.position})`);
|
||||
await sleep(1500);
|
||||
|
||||
if(count++ == 3) throw 'Block wont open';
|
||||
}
|
||||
|
||||
return this.bot.currentWindow;
|
||||
}
|
||||
|
||||
async openCraftingTable(block){
|
||||
|
||||
Reference in New Issue
Block a user