由 Fabric 客户端模组 AutoStore v1.3.1 重构为独立 Node.js 机器人,无需游戏客户端。 核心实现: - 领土数据:Dynmap markers.json 拉取解析(奇偶填充识别国中飞地)、SimpCore 开放 API 第四数据源、 带缓存击穿的时间戳请求与内容未变化跳过解析 - 扩地规划:四种策略(方形外扩 / 区域 S 形扫描 / 玩家周围 / 绕国一圈)逐行对照移植, 接壤判定只算己方领土,飞地优先填补 - 扩地状态机:规划→寻路→等待开垦→成功/失败→下一目标→完成,含超时重试、 往边上走两步自救、连续超时回城、耐久保护回城 - 生存辅助:自动进食、自动攻击(豁免中立生物)、自动游泳防溺水、自动用船(默认关闭) - 寻路:Baritone → mineflayer-pathfinder,高度偏好用包装 Goal 等价移植 Grim 反作弊合规层(新增,核心差异点): - 旋转不超前:接管 bot.look 按 ≤8.6°/tick 推进朝向,消除 mineflayer 「本地朝向瞬间跳变、数据包滞后补发」导致的 Prediction 分歧 - 攻击三重校验:朝向已同步 + 距离 ≤3.0 格 + 视线可达 + 攻击冷却 + 非骑乘/进食 - 动作互斥:拦下疾跑+潜行、疾跑+使用物品等原版不可能出现的组合 - 物理参数只读自检 + 寻路约束(禁挖/禁放/禁跑酷/禁搭塔/限下落/绕开水域) 网页控制台(新增): - 状态面板、Canvas 领土地图(缩放/平移/图层)、分组配置表单、实时日志流 - REST + WebSocket 接口,支持 WEB_TOKEN 令牌鉴权 修复: - containsFuzzy 只取固定长度前缀的末行编辑距离,导致完全相同的子串也匹配不上 - 占领去重依赖 Set.add 返回值(JS 中恒为真),导致同一区块被重复计数 - 非法枚举值不再钳制到最近边界,改为回退默认值 测试:154 个用例,npm test 全绿;npm run check 语法自检通过。
583 行
20 KiB
JavaScript
583 行
20 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* Grim 反作弊合规层。
|
||
*
|
||
* 目标:让 mineflayer 机器人在装有 GrimAC 的服务器上「行为上像一个正常原版客户端」。
|
||
*
|
||
* 核心手段(每一项都对应一类 Grim 检查):
|
||
*
|
||
* 1. **旋转不超前**(防 Prediction / Aim 类检查)
|
||
* mineflayer 的 `bot.look()` 会立刻把 `bot.entity.yaw` 改到目标值,
|
||
* 而朝向数据包是按 `physics.yawSpeed` 逐 tick 慢慢补发的。
|
||
* 这会造成「本地已经朝新方向走、服务端还以为朝旧方向」的预测分歧 —— Grim 的
|
||
* Prediction 检查正是逐 tick 复算玩家位置并与上报位置比对,分歧必然标记。
|
||
* 因此本层接管 `bot.look`:只把目标朝向记下来,在每个 physicsTick 里把
|
||
* `bot.entity.yaw/pitch` 按 ≤ `grim.maxRotationPerTickDeg` 的步长推进,
|
||
* 保证「本地朝向」永远等于「服务端已收到的朝向」,最多差 1 tick。
|
||
*
|
||
* 2. **攻击前三重校验**(防 Reach / LineOfSight / Post / MultiActions)
|
||
* - 旋转已同步(服务端认为你正看着目标)
|
||
* - 眼睛到目标碰撞箱的距离 ≤ `grim.attackReach`(生存模式 3.0)
|
||
* - 视线可达(中间没有实心方块遮挡)
|
||
* - 攻击冷却已好(1.9+ 武器攻速)
|
||
* - 不处于骑乘 / 进食 / 使用物品状态
|
||
*
|
||
* 3. **动作互斥**(防 MultiActions)
|
||
* 疾跑 + 潜行、疾跑 + 使用物品、疾跑 + 跳跃(可配)等原版客户端不会出现的组合一律拦下。
|
||
*
|
||
* 4. **物理参数只读校验**(防「改物理常数绕过预测」这类自杀式操作)
|
||
* `audit()` 会核对 `bot.physics` 的关键常量是否仍为原版值。
|
||
*/
|
||
|
||
/** 物理仿真步长(秒),与 mineflayer 的 PHYSICS_INTERVAL_MS=50 对应 */
|
||
const PHYSICS_TIMESTEP = 0.05;
|
||
|
||
/** 原版物理常量基线,用于 audit() 校验未被篡改 */
|
||
const VANILLA_PHYSICS_BASELINE = {
|
||
gravity: 0.08,
|
||
playerSpeed: 0.1,
|
||
sprintSpeed: 0.3,
|
||
sneakSpeed: 0.3,
|
||
stepHeight: 0.6,
|
||
airborneInertia: 0.91,
|
||
waterInertia: 0.8
|
||
};
|
||
|
||
/** 攻击速度(次/秒)→ 冷却毫秒;按物品名匹配,未命中回退 4.0(空手) */
|
||
const ATTACK_SPEED_RULES = [
|
||
[/^netherite_sword$|^diamond_sword$|^iron_sword$|^golden_sword$|^stone_sword$|^wooden_sword$/, 1.6],
|
||
[/^netherite_axe$|^diamond_axe$/, 1.0],
|
||
[/^iron_axe$/, 0.9],
|
||
[/^golden_axe$|^stone_axe$|^wooden_axe$/, 0.8],
|
||
[/^trident$/, 1.1],
|
||
[/^.*_pickaxe$/, 1.2],
|
||
[/^.*_shovel$/, 1.0],
|
||
[/^.*_hoe$/, 1.0]
|
||
];
|
||
|
||
/** 命中这些方块名视为「不遮挡视线」 */
|
||
const NON_OCCLUDING = new Set([
|
||
'air', 'cave_air', 'void_air', 'water', 'lava', 'seagrass', 'tall_seagrass',
|
||
'kelp', 'kelp_plant', 'bubble_column', 'torch', 'wall_torch', 'soul_torch',
|
||
'soul_wall_torch', 'redstone_torch', 'redstone_wall_torch', 'lever', 'tripwire',
|
||
'tripwire_hook', 'vine', 'short_grass', 'grass', 'fern', 'large_fern', 'dead_bush',
|
||
'sugar_cane', 'lily_pad', 'rail', 'powered_rail', 'detector_rail', 'activator_rail'
|
||
]);
|
||
|
||
class GrimCompat {
|
||
/**
|
||
* @param {import('mineflayer').Bot} bot
|
||
* @param {object} config 完整配置对象(读取 config.grim / config.allowSprint 等)
|
||
* @param {import('../util/logger').Logger} [logger]
|
||
*/
|
||
constructor(bot, config, logger) {
|
||
this.bot = bot;
|
||
this.config = config;
|
||
this.logger = logger || null;
|
||
/** 目标朝向(弧度) */
|
||
this.targetYaw = bot.entity ? bot.entity.yaw : 0;
|
||
this.targetPitch = bot.entity ? bot.entity.pitch : 0;
|
||
/** 上一 tick 的朝向,用于限速 */
|
||
this.lastAppliedYaw = this.targetYaw;
|
||
this.lastAppliedPitch = this.targetPitch;
|
||
/** 等待旋转完成的 resolver 列表 */
|
||
this._rotationWaiters = [];
|
||
/** 最近一次攻击时间戳(毫秒) */
|
||
this._lastAttackAt = 0;
|
||
/** 拦截到的不合规动作计数,供 Web 控制台展示 */
|
||
this.blockedActions = 0;
|
||
this._installed = false;
|
||
this._originalLook = null;
|
||
this._originalLookAt = null;
|
||
this._originalSetControlState = null;
|
||
this._onPhysicsTick = this._onPhysicsTick.bind(this);
|
||
}
|
||
|
||
/** 单 tick 允许的最大旋转弧度(取配置值与 mineflayer 物理限速的较小者) */
|
||
get maxStepRad() {
|
||
const configured = degToRad(this.config.grim.maxRotationPerTickDeg);
|
||
const physicsLimit = PHYSICS_TIMESTEP * (this.bot.physics ? this.bot.physics.yawSpeed : 3.0);
|
||
return Math.min(configured, physicsLimit);
|
||
}
|
||
|
||
/**
|
||
* 安装合规层:接管 look / setControlState,并挂上 physicsTick 推进器。
|
||
* 必须在 bot 完成 spawn 之后调用(此时 bot.look / bot.physics 才存在)。
|
||
*/
|
||
install() {
|
||
if (this._installed) return;
|
||
const bot = this.bot;
|
||
if (!this.config.grim.enabled) {
|
||
this._installed = true;
|
||
return;
|
||
}
|
||
this.targetYaw = bot.entity.yaw;
|
||
this.targetPitch = bot.entity.pitch;
|
||
this.lastAppliedYaw = bot.entity.yaw;
|
||
this.lastAppliedPitch = bot.entity.pitch;
|
||
|
||
this._originalLook = bot.look.bind(bot);
|
||
this._originalLookAt = bot.lookAt.bind(bot);
|
||
this._originalSetControlState = bot.setControlState.bind(bot);
|
||
|
||
bot.look = (yaw, pitch) => this.setLook(yaw, pitch === undefined ? this.targetPitch : pitch);
|
||
bot.lookAt = (point) => this.lookAtPoint(point);
|
||
bot.setControlState = (control, state) => this.setControlState(control, state);
|
||
|
||
bot.on('physicsTick', this._onPhysicsTick);
|
||
bot.on('end', () => this._releaseRotationWaiters());
|
||
this._installed = true;
|
||
this._log('Grim 合规层已启用(旋转限速 ' + this.config.grim.maxRotationPerTickDeg + '°/tick,'
|
||
+ '攻击距离 ' + this.config.grim.attackReach + ' 格)');
|
||
}
|
||
|
||
/** 卸载合规层(恢复原始方法,主要供测试使用) */
|
||
uninstall() {
|
||
if (!this._installed) return;
|
||
const bot = this.bot;
|
||
bot.removeListener('physicsTick', this._onPhysicsTick);
|
||
if (this._originalLook) bot.look = this._originalLook;
|
||
if (this._originalLookAt) bot.lookAt = this._originalLookAt;
|
||
if (this._originalSetControlState) bot.setControlState = this._originalSetControlState;
|
||
this._installed = false;
|
||
}
|
||
|
||
// ==================== 旋转 ====================
|
||
|
||
/**
|
||
* 设置目标朝向(不立即生效,由 physicsTick 逐步推进)。
|
||
* @param {number} yaw 目标 yaw(弧度)
|
||
* @param {number} [pitch] 目标 pitch(弧度)
|
||
* @returns {Promise<void>} 旋转真正同步到服务端后 resolve
|
||
*/
|
||
setLook(yaw, pitch) {
|
||
if (typeof yaw !== 'number' || Number.isNaN(yaw)) return Promise.resolve();
|
||
this.targetYaw = yaw;
|
||
this.targetPitch = typeof pitch === 'number' && !Number.isNaN(pitch)
|
||
? clamp(pitch, -Math.PI / 2, Math.PI / 2)
|
||
: this.targetPitch;
|
||
if (this.isRotationSettled()) return Promise.resolve();
|
||
return new Promise((resolve) => {
|
||
this._rotationWaiters.push(resolve);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 朝指定世界坐标看过去。
|
||
* @param {{x: number, y: number, z: number}} point
|
||
* @returns {Promise<void>}
|
||
*/
|
||
lookAtPoint(point) {
|
||
const eye = this.bot.entity.position.offset(0, this.bot.entity.eyeHeight || 1.62, 0);
|
||
const dx = point.x - eye.x;
|
||
const dy = point.y - eye.y;
|
||
const dz = point.z - eye.z;
|
||
const yaw = Math.atan2(-dx, -dz);
|
||
const groundDistance = Math.sqrt(dx * dx + dz * dz);
|
||
const pitch = Math.atan2(dy, groundDistance);
|
||
return this.setLook(yaw, pitch);
|
||
}
|
||
|
||
/**
|
||
* 看向实体(取碰撞箱中心)。
|
||
* @param {object} entity
|
||
* @returns {Promise<void>}
|
||
*/
|
||
lookAtEntity(entity) {
|
||
const height = entity.height || 1.8;
|
||
return this.lookAtPoint({
|
||
x: entity.position.x,
|
||
y: entity.position.y + height * 0.85,
|
||
z: entity.position.z
|
||
});
|
||
}
|
||
|
||
/** @returns {boolean} 当前朝向是否已经与目标一致(即服务端已收到完整旋转) */
|
||
isRotationSettled() {
|
||
return Math.abs(shortestAngleDelta(this.bot.entity.yaw, this.targetYaw)) < 1e-4
|
||
&& Math.abs(this.bot.entity.pitch - this.targetPitch) < 1e-4;
|
||
}
|
||
|
||
/**
|
||
* 等待旋转同步完成。
|
||
* @param {number} [timeoutMs] 超时(默认 3000ms)
|
||
* @returns {Promise<boolean>} 是否在超时前完成
|
||
*/
|
||
awaitRotationSettled(timeoutMs) {
|
||
if (this.isRotationSettled()) return Promise.resolve(true);
|
||
const limit = timeoutMs === undefined ? 3000 : timeoutMs;
|
||
return new Promise((resolve) => {
|
||
let done = false;
|
||
const timer = setTimeout(() => {
|
||
if (done) return;
|
||
done = true;
|
||
resolve(false);
|
||
}, limit);
|
||
this._rotationWaiters.push(() => {
|
||
if (done) return;
|
||
done = true;
|
||
clearTimeout(timer);
|
||
resolve(true);
|
||
});
|
||
});
|
||
}
|
||
|
||
/** physicsTick:把 entity.yaw/pitch 朝目标推进一小步(mineflayer 会在同一 tick 把该朝向发包) */
|
||
_onPhysicsTick() {
|
||
const bot = this.bot;
|
||
if (!bot.entity) return;
|
||
const step = this.maxStepRad;
|
||
const yawDelta = shortestAngleDelta(bot.entity.yaw, this.targetYaw);
|
||
if (Math.abs(yawDelta) > 1e-6) {
|
||
bot.entity.yaw += clamp(yawDelta, -step, step);
|
||
} else {
|
||
bot.entity.yaw = this.targetYaw;
|
||
}
|
||
const pitchDelta = this.targetPitch - bot.entity.pitch;
|
||
if (Math.abs(pitchDelta) > 1e-6) {
|
||
bot.entity.pitch += clamp(pitchDelta, -step, step);
|
||
} else {
|
||
bot.entity.pitch = this.targetPitch;
|
||
}
|
||
if (this.isRotationSettled()) this._releaseRotationWaiters();
|
||
}
|
||
|
||
_releaseRotationWaiters() {
|
||
const waiters = this._rotationWaiters;
|
||
this._rotationWaiters = [];
|
||
for (const resolve of waiters) {
|
||
try {
|
||
resolve();
|
||
} catch {
|
||
// 忽略订阅者异常
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 控制键 ====================
|
||
|
||
/**
|
||
* 带互斥规则的控制键设置(替换 bot.setControlState)。
|
||
*
|
||
* 拦下的组合(原版客户端不可能出现,Grim 的 MultiActions 会标记):
|
||
* - 疾跑 + 潜行
|
||
* - 疾跑 + 使用物品(进食 / 拉弓 / 举盾)
|
||
* - 疾跑 + 跳跃(仅在 grim.blockSprintJump 开启时拦,默认放行,因为原版合法)
|
||
*
|
||
* @param {string} control 'forward'|'back'|'left'|'right'|'jump'|'sprint'|'sneak'
|
||
* @param {boolean} state
|
||
*/
|
||
setControlState(control, state) {
|
||
const bot = this.bot;
|
||
if (!this.config.grim.enabled) {
|
||
this._originalSetControlState(control, state);
|
||
return;
|
||
}
|
||
if (state && this.config.grim.blockConflictingActions) {
|
||
const conflicting = this._findConflict(control);
|
||
if (conflicting) {
|
||
this.blockedActions++;
|
||
this._log(`已拦下不合规动作组合:${control} + ${conflicting}`);
|
||
return;
|
||
}
|
||
}
|
||
if (state && control === 'sprint' && this.config.grim.blockSprintJump && bot.getControlState('jump')) {
|
||
this.blockedActions++;
|
||
return;
|
||
}
|
||
this._originalSetControlState(control, state);
|
||
}
|
||
|
||
/** @returns {string|null} 与指定控制键冲突且当前已按下的控制键 */
|
||
_findConflict(control) {
|
||
const bot = this.bot;
|
||
const sprinting = control === 'sprint' || bot.getControlState('sprint');
|
||
if (control === 'sneak' && sprinting) return 'sprint';
|
||
if (control === 'sprint' && bot.getControlState('sneak')) return 'sneak';
|
||
if (this.config.grim.stopMovementWhileEating && this.isUsingItem()) {
|
||
if (control === 'sprint' || control === 'forward' || control === 'jump') return 'use_item';
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** 松开所有移动控制键 */
|
||
releaseAllControls() {
|
||
for (const control of ['forward', 'back', 'left', 'right', 'jump', 'sprint', 'sneak']) {
|
||
try {
|
||
this._originalSetControlState
|
||
? this._originalSetControlState(control, false)
|
||
: this.bot.setControlState(control, false);
|
||
} catch {
|
||
// 忽略
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 攻击 ====================
|
||
|
||
/** @returns {boolean} 是否正在使用物品(进食 / 拉弓 / 举盾) */
|
||
isUsingItem() {
|
||
return this.bot.usingHeldItem === true;
|
||
}
|
||
|
||
/**
|
||
* 当前手持物品的两次攻击之间的冷却时间(毫秒)。
|
||
* @returns {number}
|
||
*/
|
||
attackCooldownMs() {
|
||
const item = this.bot.heldItem;
|
||
const name = item && item.name ? item.name : '';
|
||
for (const [pattern, speed] of ATTACK_SPEED_RULES) {
|
||
if (pattern.test(name)) return Math.round(1000 / speed);
|
||
}
|
||
return Math.round(1000 / 4.0); // 空手 / 其它物品:4 次每秒
|
||
}
|
||
|
||
/** @returns {number} 距离下次可攻击还剩多少毫秒(0 表示已冷却好) */
|
||
attackCooldownRemainingMs() {
|
||
const remaining = this.attackCooldownMs() - (Date.now() - this._lastAttackAt);
|
||
return remaining > 0 ? remaining : 0;
|
||
}
|
||
|
||
/**
|
||
* 检查是否可以攻击目标(不实际攻击)。
|
||
* @param {object} entity
|
||
* @returns {{ok: boolean, reason?: string}}
|
||
*/
|
||
canAttack(entity) {
|
||
const bot = this.bot;
|
||
if (!entity || !entity.position || !entity.isValid) {
|
||
return { ok: false, reason: 'invalid_entity' };
|
||
}
|
||
if (this.config.grim.blockActionsWhileRiding && bot.vehicle) {
|
||
return { ok: false, reason: 'riding' };
|
||
}
|
||
if (this.isUsingItem()) {
|
||
return { ok: false, reason: 'using_item' };
|
||
}
|
||
const distance = eyeDistanceToEntity(bot, entity);
|
||
if (distance > this.config.grim.attackReach) {
|
||
return { ok: false, reason: 'out_of_reach' };
|
||
}
|
||
if (this.config.grim.respectAttackCooldown && this.attackCooldownRemainingMs() > 0) {
|
||
return { ok: false, reason: 'cooldown' };
|
||
}
|
||
if (this.config.grim.requireLineOfSight && !this.hasLineOfSight(entity)) {
|
||
return { ok: false, reason: 'no_line_of_sight' };
|
||
}
|
||
return { ok: true };
|
||
}
|
||
|
||
/**
|
||
* 视线可达判定:从眼睛朝目标碰撞箱中心射线,中途不得被实心方块遮挡。
|
||
* @param {object} entity
|
||
* @returns {boolean}
|
||
*/
|
||
hasLineOfSight(entity) {
|
||
const bot = this.bot;
|
||
if (!bot.world || typeof bot.world.raycast !== 'function') return true;
|
||
const eye = bot.entity.position.offset(0, bot.entity.eyeHeight || 1.62, 0);
|
||
const target = {
|
||
x: entity.position.x,
|
||
y: entity.position.y + (entity.height || 1.8) * 0.5,
|
||
z: entity.position.z
|
||
};
|
||
const dx = target.x - eye.x;
|
||
const dy = target.y - eye.y;
|
||
const dz = target.z - eye.z;
|
||
const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||
if (length < 1e-6) return true;
|
||
const direction = { x: dx / length, y: dy / length, z: dz / length };
|
||
try {
|
||
const hit = bot.world.raycast(eye, direction, length, (block) => {
|
||
return block && block.boundingBox !== 'empty' && !NON_OCCLUDING.has(block.name);
|
||
});
|
||
return !hit;
|
||
} catch {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行一次合规攻击:先同步朝向,再校验距离 / 视线 / 冷却,最后出手。
|
||
*
|
||
* @param {object} entity 目标实体
|
||
* @param {{skipRotation?: boolean}} [options]
|
||
* @returns {Promise<boolean>} 是否成功出手
|
||
*/
|
||
async attackEntity(entity, options) {
|
||
const opts = options || {};
|
||
if (!this.config.grim.enabled) {
|
||
bot_attack(this.bot, entity);
|
||
this._lastAttackAt = Date.now();
|
||
return true;
|
||
}
|
||
if (!opts.skipRotation) {
|
||
await this.lookAtEntity(entity);
|
||
const settled = await this.awaitRotationSettled(600);
|
||
if (!settled) return false;
|
||
}
|
||
const check = this.canAttack(entity);
|
||
if (!check.ok) return false;
|
||
bot_attack(this.bot, entity);
|
||
this._lastAttackAt = Date.now();
|
||
return true;
|
||
}
|
||
|
||
// ==================== 使用物品 ====================
|
||
|
||
/**
|
||
* 合规地使用一次手持物品(进食 / 放船等)。
|
||
*
|
||
* 原版客户端在使用物品时会自动取消疾跑并减速,这里显式松开移动键,
|
||
* 避免 Grim 的 NoSlow / MultiActions 检查。
|
||
*
|
||
* @param {{holdMs?: number, stopMovement?: boolean}} [options]
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async useItem(options) {
|
||
const opts = options || {};
|
||
const bot = this.bot;
|
||
if (this.config.grim.blockActionsWhileRiding && bot.vehicle) return false;
|
||
if (this.config.grim.stopMovementWhileEating && opts.stopMovement !== false) {
|
||
for (const control of ['forward', 'back', 'left', 'right', 'sprint', 'jump']) {
|
||
this.setControlState(control, false);
|
||
}
|
||
// 等待一个 tick,让服务端看到「已停止移动」后再开始使用物品
|
||
await bot.waitForTicks(1).catch(() => {});
|
||
}
|
||
bot.activateItem();
|
||
const holdMs = opts.holdMs === undefined ? 100 : opts.holdMs;
|
||
await sleep(holdMs);
|
||
return true;
|
||
}
|
||
|
||
/** 结束使用物品(进食完成 / 放船完成后调用) */
|
||
stopUsingItem() {
|
||
try {
|
||
this.bot.deactivateItem();
|
||
} catch {
|
||
// 忽略
|
||
}
|
||
}
|
||
|
||
// ==================== 自检 ====================
|
||
|
||
/**
|
||
* 物理与游戏状态自检:确认没有被改成非原版状态。
|
||
* @returns {{ok: boolean, issues: string[]}}
|
||
*/
|
||
audit() {
|
||
const bot = this.bot;
|
||
const issues = [];
|
||
if (!bot.physics) {
|
||
issues.push('bot.physics 不存在,物理引擎未启用');
|
||
} else if (this.config.grim.checkPhysicsIntegrity) {
|
||
for (const [key, expected] of Object.entries(VANILLA_PHYSICS_BASELINE)) {
|
||
const actual = bot.physics[key];
|
||
if (typeof actual === 'number' && Math.abs(actual - expected) > 1e-9) {
|
||
issues.push(`物理常量 ${key} 被改为 ${actual}(原版应为 ${expected})`);
|
||
}
|
||
}
|
||
}
|
||
if (bot.physicsEnabled === false) {
|
||
issues.push('bot.physicsEnabled 为 false,移动不会走本地预测,极易触发 Grim 的 Prediction 检查');
|
||
}
|
||
if (bot.game && bot.game.gameMode === 'creative') {
|
||
issues.push('当前为创造模式,飞行/瞬间破坏会直接触发 Grim 检查');
|
||
}
|
||
if (bot.entity && bot.entity.elytraFlying) {
|
||
issues.push('正在鞘翅飞行,mineflayer 的鞘翅物理与原版不一致');
|
||
}
|
||
if (bot.creative && bot.creative.flying) {
|
||
issues.push('bot.creative.flying 为 true,必须关闭');
|
||
}
|
||
return { ok: issues.length === 0, issues };
|
||
}
|
||
|
||
/** @returns {object} 供 Web 控制台展示的合规状态快照 */
|
||
snapshot() {
|
||
const audit = this.audit();
|
||
return {
|
||
enabled: this.config.grim.enabled,
|
||
maxRotationPerTickDeg: this.config.grim.maxRotationPerTickDeg,
|
||
effectiveStepDeg: Number(radToDeg(this.maxStepRad).toFixed(2)),
|
||
attackReach: this.config.grim.attackReach,
|
||
rotationSettled: this.isRotationSettled(),
|
||
attackCooldownMs: this.attackCooldownRemainingMs(),
|
||
blockedActions: this.blockedActions,
|
||
physicsOk: audit.ok,
|
||
issues: audit.issues
|
||
};
|
||
}
|
||
|
||
_log(message) {
|
||
if (this.logger) this.logger.log(message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 计算从 from 到 to 的最短角差(结果落在 (-π, π])。
|
||
* @param {number} from
|
||
* @param {number} to
|
||
* @returns {number}
|
||
*/
|
||
function shortestAngleDelta(from, to) {
|
||
const TWO_PI = Math.PI * 2;
|
||
let delta = (to - from) % TWO_PI;
|
||
if (delta > Math.PI) delta -= TWO_PI;
|
||
else if (delta < -Math.PI) delta += TWO_PI;
|
||
return delta;
|
||
}
|
||
|
||
/**
|
||
* 眼睛到实体碰撞箱的最近距离。
|
||
* @param {import('mineflayer').Bot} bot
|
||
* @param {object} entity
|
||
* @returns {number}
|
||
*/
|
||
function eyeDistanceToEntity(bot, entity) {
|
||
const eye = bot.entity.position.offset(0, bot.entity.eyeHeight || 1.62, 0);
|
||
const half = (entity.width || 0.6) / 2;
|
||
const height = entity.height || 1.8;
|
||
const cx = clamp(eye.x, entity.position.x - half, entity.position.x + half);
|
||
const cy = clamp(eye.y, entity.position.y, entity.position.y + height);
|
||
const cz = clamp(eye.z, entity.position.z - half, entity.position.z + half);
|
||
const dx = cx - eye.x;
|
||
const dy = cy - eye.y;
|
||
const dz = cz - eye.z;
|
||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||
}
|
||
|
||
function bot_attack(bot, entity) {
|
||
bot.attack(entity, true);
|
||
}
|
||
|
||
function clamp(value, min, max) {
|
||
return Math.min(max, Math.max(min, value));
|
||
}
|
||
|
||
function degToRad(deg) {
|
||
return (deg * Math.PI) / 180;
|
||
}
|
||
|
||
function radToDeg(rad) {
|
||
return (rad * 180) / Math.PI;
|
||
}
|
||
|
||
function sleep(ms) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
module.exports = {
|
||
GrimCompat,
|
||
PHYSICS_TIMESTEP,
|
||
VANILLA_PHYSICS_BASELINE,
|
||
ATTACK_SPEED_RULES,
|
||
shortestAngleDelta,
|
||
eyeDistanceToEntity,
|
||
degToRad,
|
||
radToDeg
|
||
};
|