文件
BotStore/src/core/territoryModel.js
T
lyizai a12a24c716 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 语法自检通过。
2026-09-27 17:17:41 +08:00

255 行
8.5 KiB
JavaScript
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
'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 };