feat: 用 Node.js + mineflayer 重构 AutoStore,UI 改为网页控制台并适配 Grim 反作弊
由 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 语法自检通过。
这个提交包含在:
@@ -0,0 +1,135 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
|
||||
const { TerritoryModel } = require('../src/core/territoryModel');
|
||||
const chunkPos = require('../src/core/chunkPos');
|
||||
|
||||
/**
|
||||
* 构造一个邦国状态。
|
||||
* @param {string} name
|
||||
* @param {Array<[number, number]>} coords
|
||||
*/
|
||||
function makeState(name, coords, capital) {
|
||||
const chunks = new Set(coords.map(([x, z]) => chunkPos.key(x, z)));
|
||||
return {
|
||||
name,
|
||||
capital: capital || { x: coords[0][0], z: coords[0][1] },
|
||||
chunks,
|
||||
color: 0x75bd20
|
||||
};
|
||||
}
|
||||
|
||||
test('构造时建立 allClaimed 并集', () => {
|
||||
const model = new TerritoryModel(new Map([
|
||||
['A', makeState('A', [[0, 0], [1, 0]])],
|
||||
['B', makeState('B', [[5, 5]])]
|
||||
]));
|
||||
assert.strictEqual(model.stateCount, 2);
|
||||
assert.strictEqual(model.allClaimedChunks().size, 3);
|
||||
assert.ok(model.allClaimedChunks().has(chunkPos.key(1, 0)));
|
||||
});
|
||||
|
||||
test('findContainingState 找到覆盖区块的邦国', () => {
|
||||
const model = new TerritoryModel(new Map([
|
||||
['A', makeState('A', [[0, 0], [1, 0]])],
|
||||
['B', makeState('B', [[5, 5]])]
|
||||
]));
|
||||
assert.strictEqual(model.findContainingState({ x: 1, z: 0 }).name, 'A');
|
||||
assert.strictEqual(model.findContainingState({ x: 5, z: 5 }).name, 'B');
|
||||
assert.strictEqual(model.findContainingState({ x: 9, z: 9 }), null);
|
||||
});
|
||||
|
||||
test('飞地识别:四周被同一邦国包围的空洞', () => {
|
||||
// 3×3 中空方块:中心 (1,1) 是飞地
|
||||
const coords = [];
|
||||
for (let x = 0; x <= 2; x++) {
|
||||
for (let z = 0; z <= 2; z++) {
|
||||
if (x === 1 && z === 1) continue;
|
||||
coords.push([x, z]);
|
||||
}
|
||||
}
|
||||
const model = new TerritoryModel(new Map([['A', makeState('A', coords)]]));
|
||||
const flying = model.selfFlyingChunks();
|
||||
// 未设置己方邦国名时为空集
|
||||
assert.strictEqual(flying.size, 0);
|
||||
model.setSelfStateName('A');
|
||||
const selfFlying = model.selfFlyingChunks();
|
||||
assert.strictEqual(selfFlying.size, 1);
|
||||
assert.ok(selfFlying.has(chunkPos.key(1, 1)));
|
||||
assert.strictEqual(model.flyingChunks().size, 1);
|
||||
});
|
||||
|
||||
test('飞地不包含被其他邦国占领的区块', () => {
|
||||
const aCoords = [];
|
||||
for (let x = 0; x <= 2; x++) {
|
||||
for (let z = 0; z <= 2; z++) {
|
||||
if (x === 1 && z === 1) continue;
|
||||
aCoords.push([x, z]);
|
||||
}
|
||||
}
|
||||
const model = new TerritoryModel(new Map([
|
||||
['A', makeState('A', aCoords)],
|
||||
['B', makeState('B', [[1, 1]])]
|
||||
]));
|
||||
model.setSelfStateName('A');
|
||||
assert.strictEqual(model.selfFlyingChunks().size, 0);
|
||||
});
|
||||
|
||||
test('addSessionClaim 同步更新会话集合、全局集合与己方邦国', () => {
|
||||
const model = new TerritoryModel(new Map([['A', makeState('A', [[0, 0]])]]));
|
||||
model.setSelfStateName('A');
|
||||
model.addSessionClaim({ x: 1, z: 0 });
|
||||
assert.ok(model.sessionClaimsSet().has(chunkPos.key(1, 0)));
|
||||
assert.ok(model.allClaimedChunks().has(chunkPos.key(1, 0)));
|
||||
assert.ok(model.selfState().chunks.has(chunkPos.key(1, 0)));
|
||||
});
|
||||
|
||||
test('addSessionClaim 后飞地缓存失效并重算', () => {
|
||||
const coords = [];
|
||||
for (let x = 0; x <= 2; x++) {
|
||||
for (let z = 0; z <= 2; z++) {
|
||||
if (x === 1 && z === 1) continue;
|
||||
coords.push([x, z]);
|
||||
}
|
||||
}
|
||||
const model = new TerritoryModel(new Map([['A', makeState('A', coords)]]));
|
||||
model.setSelfStateName('A');
|
||||
assert.strictEqual(model.selfFlyingChunks().size, 1);
|
||||
// 填上飞地后飞地应为空
|
||||
model.addSessionClaim({ x: 1, z: 1 });
|
||||
assert.strictEqual(model.selfFlyingChunks().size, 0);
|
||||
});
|
||||
|
||||
test('mergeApiData:API 独有邦国新增、共有邦国取并集且 Dynmap 颜色优先', () => {
|
||||
const model = new TerritoryModel(new Map([['A', makeState('A', [[0, 0]])]]));
|
||||
const apiChunks = new Map([
|
||||
['A', new Set([chunkPos.key(1, 0)])],
|
||||
['C', new Set([chunkPos.key(9, 9)])]
|
||||
]);
|
||||
const apiCapitals = new Map([['C', { x: 9, z: 9 }]]);
|
||||
model.mergeApiData(apiChunks, apiCapitals, 0xff43de);
|
||||
|
||||
const a = model.state('A');
|
||||
assert.strictEqual(a.chunks.size, 2, 'A 的区块应为并集');
|
||||
assert.strictEqual(a.color, 0x75bd20, 'Dynmap 已有邦国应保留原颜色');
|
||||
|
||||
const c = model.state('C');
|
||||
assert.ok(c, 'API 独有邦国应被新增');
|
||||
assert.strictEqual(c.color, 0xff43de);
|
||||
assert.deepStrictEqual(c.capital, { x: 9, z: 9 });
|
||||
assert.ok(model.allClaimedChunks().has(chunkPos.key(9, 9)));
|
||||
});
|
||||
|
||||
test('mergeApiData:Dynmap 王城优先于 API 王城', () => {
|
||||
const model = new TerritoryModel(new Map([['A', makeState('A', [[0, 0]], { x: 7, z: 7 })]]));
|
||||
model.mergeApiData(new Map([['A', new Set([chunkPos.key(1, 0)])]]), new Map([['A', { x: 99, z: 99 }]]), 0);
|
||||
assert.deepStrictEqual(model.state('A').capital, { x: 7, z: 7 });
|
||||
});
|
||||
|
||||
test('mergeApiData:空输入直接返回不改动模型', () => {
|
||||
const model = new TerritoryModel(new Map([['A', makeState('A', [[0, 0]])]]));
|
||||
model.mergeApiData(new Map(), new Map(), 0);
|
||||
assert.strictEqual(model.stateCount, 1);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户