fix: 定位「寻路恒定 47 节点」的根因(专属配置被冻结)并改为按进度判超时
找到「寻路怎么调都只探索 47 个节点」的真凶 - 启动任务时若带了配置,会被整份存为该机器人的专属配置覆盖。它是一份**完整快照**, 等于把当时那一刻的全局配置冻结在这个机器人身上,之后改全局配置再也影响不到它。 实测这台机器人的覆盖里 allowBreak/allowParkour 都是 false,所以我前面所有 「放开挖方块 / 跑酷 / 不限搜索半径」的排查全部无效,一直在跟同一份旧配置较劲。 - 清除覆盖后寻路器的邻居生成数从 **2 变成 15**,机器人开始真正移动。 - 现在保存覆盖时会明确记一条日志(含策略/邦国/数量),说明「此后全局改动不影响它」。 - 新增「清除专属配置」按钮与 POST /api/bots/:id/settings/clear-config, 并在配置面板提示该机器人是否有专属覆盖 —— 这是「改了全局却没生效」的唯一退路。 寻路超时改为按进度判定 - 原来只看墙钟时间:目标常在几百格外,而寻路速度受地形与反作弊回弹影响可能只有 正常步行的十分之一,于是「走得慢但在稳步接近」会被误判成卡死,然后 /u return 回城把进度全部清零。现在只要与目标的曼哈顿距离有实质缩短就重置这一段计时。 - 首次观察只记录距离基准、不续期,保证「原地不动」仍会照常超时。 新增寻路诊断(排查这类问题的关键手段) - GET /api/bots/:id/status 的 pathingDiag 暴露:起点可走邻居数与具体坐标、 寻路器能否通过 world.getBlock 读到方块、四向可见距离。 - 邻居数极少(如 2)说明不是目标太远,而是移动生成器从起点就找不到分支, 此时调任何搜索参数都没用。 测试 355 -> 360,全部通过。
这个提交包含在:
@@ -852,6 +852,10 @@ class BotManager extends EventEmitter {
|
||||
packetStats: Object.assign({}, this.packetStats),
|
||||
world: this._worldDiagnostics(),
|
||||
pathing: this.pathing ? this.pathing.isPathing() : false,
|
||||
/** 寻路诊断:邻居生成数、世界数据可读性、四向可见距离 */
|
||||
pathingDiag: this.pathing && typeof this.pathing.diagnostics === 'function'
|
||||
? this.pathing.diagnostics()
|
||||
: null,
|
||||
eating: this.eater ? this.eater.eating : false,
|
||||
grim: this.grim ? this.grim.snapshot() : null
|
||||
};
|
||||
|
||||
+27
-1
@@ -421,17 +421,43 @@ class BotPool extends EventEmitter {
|
||||
? normalizeConfig(overrideConfig, this.getConfigFor(id))
|
||||
: this.getConfigFor(id);
|
||||
if (overrideConfig) {
|
||||
// 控制台点「开始扩地」时顺带保存该机器人的配置
|
||||
// 控制台点「开始扩地」且勾了「仅对当前机器人生效」时,顺带把这份配置存为该机器人的专属配置。
|
||||
// 注意这是一份**完整快照**:存下之后全局配置的改动就不再影响它了,
|
||||
// 所以这里明确记一条日志,避免以后排查时一头雾水。
|
||||
const previous = this.settings[id] && this.settings[id].config ? '(覆盖已有)' : '(新建)';
|
||||
this.settings[id] = Object.assign({}, this.settings[id] || {}, { config });
|
||||
this._persistSettings();
|
||||
entry.config = config;
|
||||
entry.manager.applyConfig(config);
|
||||
this._log(`${entry.connection.username} 已保存专属配置${previous}:`
|
||||
+ `策略 ${config.strategy}、目标邦国「${config.stateName || '自动'}」、目标数量 ${config.targetCount};`
|
||||
+ '此后全局配置的改动不会影响它,可在配置面板点「清除专属配置」恢复继承');
|
||||
}
|
||||
entry.controller.config = config;
|
||||
const ok = await entry.controller.start(config);
|
||||
return { ok, message: ok ? '任务已启动' : entry.controller.status, entry };
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除某机器人的专属配置覆盖,恢复「继承全局默认配置」。
|
||||
*
|
||||
* 专属配置是一份完整快照,一旦存下,之后改全局配置就再也影响不到这个机器人 ——
|
||||
* 很容易出现「我明明改了全局设置,它却还是老行为」的困惑,所以需要一条明确的退路。
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {object} 生效后的设置快照
|
||||
*/
|
||||
clearBotConfigOverride(id) {
|
||||
const entry = this.require(id);
|
||||
if (this.settings[id]) delete this.settings[id].config;
|
||||
this._persistSettings();
|
||||
entry.config = this.getConfigFor(id);
|
||||
entry.manager.applyConfig(entry.config);
|
||||
entry.controller.config = entry.config;
|
||||
this._log(`${entry.connection.username} 的专属配置已清除,恢复继承全局默认配置`);
|
||||
return this.settingSnapshot(id);
|
||||
}
|
||||
|
||||
/** 停止某机器人的扩地任务 */
|
||||
stopTask(id) {
|
||||
const entry = this.require(id);
|
||||
|
||||
+63
-2
@@ -325,9 +325,70 @@ class PathingService {
|
||||
return lo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻路诊断:直接问寻路器「从当前位置能往哪些格子走」。
|
||||
*
|
||||
* 这是排查「A* 只探索几十个节点就 noPath」的关键手段:
|
||||
* 如果 `neighbors` 为 0 或极少,说明不是目标太远,而是**寻路器读不到世界数据**
|
||||
* (Movements 依赖 bot.world 的方块查询),此时调任何搜索参数都没用。
|
||||
*
|
||||
* @returns {object|null}
|
||||
*/
|
||||
diagnostics() {
|
||||
const bot = this.bot;
|
||||
if (!bot || !bot.entity || !bot.pathfinder) return null;
|
||||
const pos = bot.entity.position;
|
||||
const feet = { x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) };
|
||||
let neighbors = [];
|
||||
let error = '';
|
||||
try {
|
||||
const movements = bot.pathfinder.movements;
|
||||
// 寻路器内部用的就是这种节点结构(x/y/z + 代价字段)
|
||||
neighbors = movements.getNeighbors({
|
||||
x: feet.x,
|
||||
y: feet.y,
|
||||
z: feet.z,
|
||||
remainingBlocks: 0,
|
||||
cost: 0,
|
||||
hash: `${feet.x},${feet.y},${feet.z}`
|
||||
}) || [];
|
||||
} catch (err) {
|
||||
error = err && err.message ? err.message : String(err);
|
||||
}
|
||||
const world = bot.world;
|
||||
const worldApi = {
|
||||
hasGetBlock: typeof world.getBlock === 'function',
|
||||
hasGetBlockStateId: typeof world.getBlockStateId === 'function',
|
||||
hasGetColumn: typeof world.getColumn === 'function'
|
||||
};
|
||||
// 用寻路器同款 API 采样脚下方块,看它到底能不能读到世界
|
||||
let blockViaWorld = null;
|
||||
try {
|
||||
const b = world.getBlock ? world.getBlock(bot.entity.position.offset(0, -1, 0)) : null;
|
||||
blockViaWorld = b ? b.name : null;
|
||||
} catch (err) {
|
||||
blockViaWorld = 'ERR:' + (err && err.message ? err.message : String(err));
|
||||
}
|
||||
return {
|
||||
feet,
|
||||
neighborCount: neighbors.length,
|
||||
neighbors: neighbors.slice(0, 8).map((n) => ({ x: n.x, y: n.y, z: n.z, cost: n.cost })),
|
||||
error,
|
||||
worldApi,
|
||||
blockViaWorld,
|
||||
blockViaBlockAt: bot.blockAt ? (bot.blockAt(bot.entity.position.offset(0, -1, 0)) || {}).name : null,
|
||||
/** 四个方向能「看到」多远(方块):反映已加载区块范围 */
|
||||
visibleDistance: {
|
||||
north: Math.round(this._probeLoadedDistance(0, -1, 256)),
|
||||
south: Math.round(this._probeLoadedDistance(0, 1, 256)),
|
||||
east: Math.round(this._probeLoadedDistance(1, 0, 256)),
|
||||
west: Math.round(this._probeLoadedDistance(-1, 0, 256))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 取消当前寻路 */
|
||||
cancel() {
|
||||
if (!this.isAvailable()) return;
|
||||
cancel() { if (!this.isAvailable()) return;
|
||||
this._pathing = false;
|
||||
this.currentTarget = null;
|
||||
this.finalTarget = null;
|
||||
|
||||
在新工单中引用
屏蔽一个用户