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,254 @@
|
||||
'use strict';
|
||||
|
||||
const chunkPos = require('./chunkPos');
|
||||
|
||||
/**
|
||||
* 领土模型:聚合所有邦国的领土状态,并提供全局查询能力。
|
||||
*
|
||||
* 维护三份数据:
|
||||
* - states:邦国名 → 邦国领土状态(含区块集合、王城、颜色)
|
||||
* - allClaimed:所有邦国已占领区块的并集(判断区块是否已被占用)
|
||||
* - sessionClaims:本次会话新占领的区块(刷新地图时不会被覆盖)
|
||||
*
|
||||
* 区块集合一律用 `chunkPos.key(x, z)` 生成的整数键存进 Set,
|
||||
* 避免数万区块产生海量临时对象。
|
||||
*/
|
||||
|
||||
/** API 数据独有邦国在地图上的默认颜色(紫 #FF43DE,ARGB 0xFF43DE) */
|
||||
const API_DEFAULT_COLOR = 0xff43de;
|
||||
|
||||
class TerritoryModel {
|
||||
/**
|
||||
* @param {Map<string, {name: string, capital: {x: number, z: number}|null, chunks: Set<number>, color: number}>} [states]
|
||||
*/
|
||||
constructor(states) {
|
||||
/** @type {Map<string, {name: string, capital: {x: number, z: number}|null, chunks: Set<number>, color: number}>} */
|
||||
this.states = new Map(states || []);
|
||||
/** @type {Set<number>} 所有邦国已占领区块的并集 */
|
||||
this.allClaimed = new Set();
|
||||
for (const state of this.states.values()) {
|
||||
for (const k of state.chunks) this.allClaimed.add(k);
|
||||
}
|
||||
/** @type {Set<number>} 本次会话新占领的区块 */
|
||||
this.sessionClaims = new Set();
|
||||
/** @type {string} 当前选中的己方邦国名称 */
|
||||
this.selfStateName = '';
|
||||
/** @type {Map<string, Set<number>>|null} 飞地缓存 */
|
||||
this._flyingByState = null;
|
||||
/** @type {Set<number>|null} 飞地并集缓存 */
|
||||
this._allFlying = null;
|
||||
/** 数据来源时间戳,便于 Web 控制台展示新鲜度 */
|
||||
this.updatedAt = 0;
|
||||
}
|
||||
|
||||
/** @returns {TerritoryModel} 空模型(无任何邦国数据) */
|
||||
static empty() {
|
||||
return new TerritoryModel(new Map());
|
||||
}
|
||||
|
||||
/** @returns {IterableIterator<{name: string, capital: {x: number, z: number}|null, chunks: Set<number>, color: number}>} 所有邦国领土状态 */
|
||||
statesIter() {
|
||||
return this.states.values();
|
||||
}
|
||||
|
||||
/** @returns {number} 邦国数量 */
|
||||
get stateCount() {
|
||||
return this.states.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按名称查询邦国领土状态。
|
||||
* @param {string} name
|
||||
* @returns {{name: string, capital: {x: number, z: number}|null, chunks: Set<number>, color: number}|undefined}
|
||||
*/
|
||||
state(name) {
|
||||
return this.states.get(name);
|
||||
}
|
||||
|
||||
/** @returns {Set<number>} 所有邦国已占领区块的并集(勿直接修改) */
|
||||
allClaimedChunks() {
|
||||
return this.allClaimed;
|
||||
}
|
||||
|
||||
/** @returns {Set<number>} 本次会话新占领的区块(勿直接修改) */
|
||||
sessionClaimsSet() {
|
||||
return this.sessionClaims;
|
||||
}
|
||||
|
||||
/** @returns {string} 当前选中的己方邦国名称 */
|
||||
selfStateNameValue() {
|
||||
return this.selfStateName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前选中的己方邦国名称。
|
||||
* @param {string|null} name
|
||||
*/
|
||||
setSelfStateName(name) {
|
||||
this.selfStateName = name || '';
|
||||
}
|
||||
|
||||
/** @returns {object|null} 己方邦国的领土状态;未设置或不存在时为 null */
|
||||
selfState() {
|
||||
return this.states.get(this.selfStateName) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找包含指定区块的邦国。
|
||||
* @param {{x: number, z: number}} chunk
|
||||
* @returns {object|null}
|
||||
*/
|
||||
findContainingState(chunk) {
|
||||
const k = chunkPos.key(chunk.x, chunk.z);
|
||||
for (const state of this.states.values()) {
|
||||
if (state.chunks.has(k)) return state;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有邦国的飞地(疏漏区块)并集。
|
||||
*
|
||||
* 飞地指「四周(四个正方向)都被同一邦国占领、但自身未被占领」的区块,
|
||||
* 通常是自动扩地时漏掉的疏漏,需要在地图上标出并优先填补。
|
||||
*
|
||||
* @returns {Set<number>}
|
||||
*/
|
||||
flyingChunks() {
|
||||
if (this._allFlying) return this._allFlying;
|
||||
const result = new Set();
|
||||
for (const chunks of this._flyingByStateMap().values()) {
|
||||
for (const k of chunks) result.add(k);
|
||||
}
|
||||
this._allFlying = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/** @returns {Set<number>} 己方邦国的飞地(疏漏区块);未选择邦国时为空集 */
|
||||
selfFlyingChunks() {
|
||||
return this._flyingByStateMap().get(this.selfStateName) || EMPTY_SET;
|
||||
}
|
||||
|
||||
/** @returns {Map<string, Set<number>>} 各邦国飞地区块(懒计算并缓存) */
|
||||
_flyingByStateMap() {
|
||||
if (this._flyingByState) return this._flyingByState;
|
||||
const map = new Map();
|
||||
for (const state of this.states.values()) {
|
||||
map.set(state.name, this._computeFlyingChunks(state.chunks));
|
||||
}
|
||||
this._flyingByState = map;
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算某邦国的全部飞地区块:在该邦国的外接矩形内,
|
||||
* 找出「未被任何邦国占领、但四个正方向均属于该邦国」的区块。
|
||||
* @param {Set<number>} own
|
||||
* @returns {Set<number>}
|
||||
*/
|
||||
_computeFlyingChunks(own) {
|
||||
const result = new Set();
|
||||
if (own.size === 0) return result;
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let minZ = Infinity;
|
||||
let maxZ = -Infinity;
|
||||
for (const k of own) {
|
||||
const { x, z } = chunkPos.decode(k);
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (z < minZ) minZ = z;
|
||||
if (z > maxZ) maxZ = z;
|
||||
}
|
||||
for (let x = minX; x <= maxX; x++) {
|
||||
for (let z = minZ; z <= maxZ; z++) {
|
||||
const k = chunkPos.key(x, z);
|
||||
if (own.has(k) || this.allClaimed.has(k)) continue;
|
||||
if (own.has(chunkPos.key(x + 1, z)) && own.has(chunkPos.key(x - 1, z))
|
||||
&& own.has(chunkPos.key(x, z + 1)) && own.has(chunkPos.key(x, z - 1))) {
|
||||
result.add(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一次会话内新占领的区块:加入会话集合与全局已占领集合,
|
||||
* 并同步更新己方邦国的区块集合。
|
||||
* @param {{x: number, z: number}} chunk
|
||||
*/
|
||||
addSessionClaim(chunk) {
|
||||
const k = chunkPos.key(chunk.x, chunk.z);
|
||||
this.sessionClaims.add(k);
|
||||
this.allClaimed.add(k);
|
||||
this._invalidateFlying();
|
||||
const self = this.states.get(this.selfStateName);
|
||||
if (self) {
|
||||
self.chunks.add(k);
|
||||
}
|
||||
}
|
||||
|
||||
/** 失效飞地缓存 */
|
||||
_invalidateFlying() {
|
||||
this._flyingByState = null;
|
||||
this._allFlying = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 SimpCore 开放 API 的领土数据(在模型发布前、单线程内调用)。
|
||||
*
|
||||
* 合并原则(「任意一个数据源显示被占领即算占领」):
|
||||
* - API 中出现、Dynmap 中没有的区块 → 新增一个 API 邦国状态(或并入已有同名邦国);
|
||||
* - 两边的区块取并集,颜色以 Dynmap 优先(保持地图观感一致);
|
||||
* - 王城坐标:Dynmap 优先,API 数据兜底。
|
||||
*
|
||||
* API 数据块本身有服务端缓存(最多延迟 1 小时),因此只做增量补充,绝不覆盖 Dynmap 已确认的数据。
|
||||
*
|
||||
* @param {Map<string, Set<number>>} apiChunks API 返回的 邦国名 → 区块键集合
|
||||
* @param {Map<string, {x: number, z: number}>} [apiCapitals] API 返回的 邦国名 → 王城区块
|
||||
* @param {number} [apiColor] API 独有邦国使用的颜色
|
||||
*/
|
||||
mergeApiData(apiChunks, apiCapitals, apiColor) {
|
||||
if (!apiChunks || apiChunks.size === 0) return;
|
||||
const color = apiColor || API_DEFAULT_COLOR;
|
||||
const rebuilt = new Map();
|
||||
for (const [name, chunkKeys] of apiChunks) {
|
||||
if (!name) continue;
|
||||
const apiCapital = apiCapitals ? apiCapitals.get(name) : null;
|
||||
const existing = this.states.get(name);
|
||||
if (!existing) {
|
||||
const chunks = new Set(chunkKeys);
|
||||
let capital = apiCapital;
|
||||
if (!capital) {
|
||||
const first = chunks.values().next();
|
||||
capital = first.done ? { x: 0, z: 0 } : chunkPos.decode(first.value);
|
||||
}
|
||||
rebuilt.set(name, { name, capital, chunks, color });
|
||||
} else {
|
||||
const chunks = new Set(existing.chunks);
|
||||
for (const k of chunkKeys) chunks.add(k);
|
||||
let capital = existing.capital;
|
||||
if (apiCapital && (!capital || (capital.x === 0 && capital.z === 0))) {
|
||||
capital = apiCapital;
|
||||
}
|
||||
rebuilt.set(name, { name, capital, chunks, color: existing.color });
|
||||
}
|
||||
}
|
||||
// 未被 API 覆盖的邦国原样保留
|
||||
for (const [name, state] of this.states) {
|
||||
if (!rebuilt.has(name)) rebuilt.set(name, state);
|
||||
}
|
||||
this.states = rebuilt;
|
||||
this.allClaimed = new Set();
|
||||
for (const state of rebuilt.values()) {
|
||||
for (const k of state.chunks) this.allClaimed.add(k);
|
||||
}
|
||||
this._invalidateFlying();
|
||||
}
|
||||
}
|
||||
|
||||
/** 复用的空集合,避免重复分配 */
|
||||
const EMPTY_SET = new Set();
|
||||
|
||||
module.exports = { TerritoryModel, API_DEFAULT_COLOR };
|
||||
在新工单中引用
屏蔽一个用户