由 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 语法自检通过。
158 行
6.1 KiB
JavaScript
158 行
6.1 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const {
|
|
defaultConfig,
|
|
normalizeConfig,
|
|
validateConfig,
|
|
copyConfig,
|
|
STRATEGY,
|
|
WATER_MODE
|
|
} = require('../src/config/expansionConfig');
|
|
const { ConfigStore } = require('../src/config/configStore');
|
|
|
|
test('默认配置的关键默认值与原 Java 版一致', () => {
|
|
const config = defaultConfig();
|
|
assert.strictEqual(config.strategy, STRATEGY.SQUARE);
|
|
assert.strictEqual(config.targetCount, -1);
|
|
assert.strictEqual(config.waitTimeoutSeconds, 60);
|
|
assert.strictEqual(config.retries, 2);
|
|
assert.strictEqual(config.returnThresholdSeconds, 60);
|
|
assert.strictEqual(config.returnWaitSeconds, 12);
|
|
assert.strictEqual(config.sideStepMaxRounds, 3);
|
|
assert.strictEqual(config.durabilityThreshold, 15);
|
|
assert.strictEqual(config.eatThreshold, 14);
|
|
assert.strictEqual(config.refreshIntervalSeconds, 30);
|
|
assert.strictEqual(config.apiRefreshIntervalSeconds, 180);
|
|
assert.strictEqual(config.allowBreak, false);
|
|
assert.strictEqual(config.allowPlace, false);
|
|
assert.strictEqual(config.altitudeWeight, 0.1);
|
|
});
|
|
|
|
test('Grim 合规默认值偏保守', () => {
|
|
const config = defaultConfig();
|
|
assert.strictEqual(config.grim.enabled, true);
|
|
assert.strictEqual(config.grim.attackReach, 3.0);
|
|
assert.strictEqual(config.grim.requireLineOfSight, true);
|
|
assert.strictEqual(config.grim.respectAttackCooldown, true);
|
|
assert.strictEqual(config.grim.stopMovementWhileEating, true);
|
|
assert.strictEqual(config.grim.blockConflictingActions, true);
|
|
// 疾跑跳是原版合法动作,默认放行
|
|
assert.strictEqual(config.grim.blockSprintJump, false);
|
|
// mineflayer 无载具物理,用船默认关闭
|
|
assert.strictEqual(config.autoBoat, false);
|
|
assert.strictEqual(config.waterMode, WATER_MODE.AVOID);
|
|
// 挖 / 放方块默认关闭(Grim 检查严格)
|
|
assert.strictEqual(config.allowBreak, false);
|
|
assert.strictEqual(config.allowPlace, false);
|
|
assert.strictEqual(config.pathing.allowParkour, false);
|
|
assert.strictEqual(config.pathing.allow1by1towers, false);
|
|
});
|
|
|
|
test('normalizeConfig 只接受已知字段', () => {
|
|
const merged = normalizeConfig({ strategy: 3, 未知字段: 'x', grim: { attackReach: 4 } });
|
|
assert.strictEqual(merged.strategy, 3);
|
|
assert.strictEqual(merged.grim.attackReach, 4);
|
|
assert.strictEqual(merged.未知字段, undefined);
|
|
// 未提供的字段保持默认
|
|
assert.strictEqual(merged.retries, 2);
|
|
assert.strictEqual(merged.grim.requireLineOfSight, true);
|
|
});
|
|
|
|
test('normalizeConfig 对部分配置做深合并而非整体替换', () => {
|
|
const merged = normalizeConfig({ pathing: { maxDropDown: 5 } });
|
|
assert.strictEqual(merged.pathing.maxDropDown, 5);
|
|
assert.strictEqual(merged.pathing.liquidCost, 40, '未提供的子字段应保留默认值');
|
|
});
|
|
|
|
test('validateConfig 钳制越界值', () => {
|
|
const config = validateConfig({
|
|
strategy: 99,
|
|
targetCount: -5,
|
|
retries: 0,
|
|
waitTimeoutSeconds: 1,
|
|
durabilityThreshold: 0,
|
|
refreshIntervalSeconds: 1,
|
|
apiRefreshIntervalSeconds: 5,
|
|
altitudeWeight: -3,
|
|
eatThreshold: 50,
|
|
pathing: { maxDropDown: 999, liquidCost: 0 },
|
|
grim: { attackReach: 99, maxRotationPerTickDeg: 0 },
|
|
server: { commandCooldownMs: -1, reconnectDelayMs: 10 },
|
|
waterMode: '不存在的模式'
|
|
});
|
|
assert.strictEqual(config.strategy, 1, '非法策略应回退到默认策略而不是钳制到 4');
|
|
assert.strictEqual(config.targetCount, -1);
|
|
assert.strictEqual(config.retries, 1);
|
|
assert.strictEqual(config.waitTimeoutSeconds, 5);
|
|
assert.strictEqual(config.durabilityThreshold, 1);
|
|
assert.strictEqual(config.refreshIntervalSeconds, 10);
|
|
assert.strictEqual(config.apiRefreshIntervalSeconds, 60);
|
|
assert.strictEqual(config.altitudeWeight, 0);
|
|
assert.strictEqual(config.eatThreshold, 20);
|
|
assert.strictEqual(config.pathing.maxDropDown, 64);
|
|
assert.strictEqual(config.pathing.liquidCost, 1);
|
|
assert.strictEqual(config.grim.attackReach, 6);
|
|
assert.strictEqual(config.grim.maxRotationPerTickDeg, 1);
|
|
assert.strictEqual(config.server.commandCooldownMs, 0);
|
|
assert.strictEqual(config.server.reconnectDelayMs, 1000);
|
|
assert.strictEqual(config.waterMode, WATER_MODE.AVOID);
|
|
});
|
|
|
|
test('copyConfig 是深拷贝', () => {
|
|
const original = defaultConfig();
|
|
const copy = copyConfig(original);
|
|
copy.grim.attackReach = 5;
|
|
copy.server.claimCommand = 'changed';
|
|
assert.strictEqual(original.grim.attackReach, 3.0);
|
|
assert.strictEqual(original.server.claimCommand, 'u claimmode');
|
|
});
|
|
|
|
test('ConfigStore 读写往返一致,且能容忍损坏文件', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'autostore-cfg-'));
|
|
const file = path.join(dir, 'nested', 'autostore.json');
|
|
const store = new ConfigStore(file);
|
|
|
|
// 文件不存在 → 返回默认配置
|
|
const defaults = store.load();
|
|
assert.strictEqual(defaults.strategy, STRATEGY.SQUARE);
|
|
|
|
// 保存后能读回
|
|
defaults.strategy = STRATEGY.PERIMETER;
|
|
defaults.stateName = '测试国';
|
|
assert.strictEqual(store.save(defaults), true);
|
|
const loaded = store.load();
|
|
assert.strictEqual(loaded.strategy, STRATEGY.PERIMETER);
|
|
assert.strictEqual(loaded.stateName, '测试国');
|
|
|
|
// 损坏文件 → 回退到默认配置
|
|
fs.writeFileSync(file, '{ 这不是合法 JSON', 'utf8');
|
|
assert.strictEqual(store.load().strategy, STRATEGY.SQUARE);
|
|
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
test('ConfigStore 在配置文件缺失 API Key 时回退到环境变量', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'autostore-cfg-'));
|
|
const file = path.join(dir, 'autostore.json');
|
|
const store = new ConfigStore(file);
|
|
const config = defaultConfig();
|
|
config.apiKey = '';
|
|
store.save(config);
|
|
|
|
const previous = process.env.SIMP_API_KEY;
|
|
process.env.SIMP_API_KEY = 'env-key-for-test';
|
|
try {
|
|
assert.strictEqual(store.load().apiKey, 'env-key-for-test');
|
|
} finally {
|
|
if (previous === undefined) delete process.env.SIMP_API_KEY;
|
|
else process.env.SIMP_API_KEY = previous;
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|