文件
BotStore/test/bigMapService.test.js
T
lyizai 39624da2d5 feat: 支持多机器人挂机、开拓日志、公屏完整日志与大地图
多机器人(src/bot/botPool.js、src/config/botsConfig.js)
- .env 用 MC_BOTS 一次配置多个账号(JSON 数组或 用户名:密码 逗号分隔),未配置时回退单机器人
- 每个机器人独立连接 / 登录 / 扩地状态机,共享一份领土数据服务与运行日志
- 密码只从 .env 读取;data/bots.json 只存启用状态、备注名、自动开始与各自的配置覆盖
- 每个机器人可覆盖全局配置(策略 / 目标邦国 / 目标数量等),未覆盖字段回退全局默认

开拓日志(src/core/expansionLog.js)
- 逐条记录开拓玩家、邦国、区块、方块坐标、时间与判定来源,追加写入 data/expansion-log.jsonl
- 支持按机器人 / 邦国 / 时间筛选、统计、导出带 BOM 的 CSV,超过 16MB 自动轮转

游戏公屏完整日志与聊天框(src/core/chatLog.js)
- 每个机器人的全部公屏 / 系统 / 动作栏消息按机器人分文件留档,自身发出的聊天与指令同样留档
- 控制台新增公屏聊天页签与消息发送框

登录验证门禁
- 游戏侧:未完成登录验证时拒绝发送指令 / 聊天,isReady() 也不放行扩地任务;
  新增 server.loginConfirmTimeoutMs 兜底,避免服务器不回提示导致永远卡在未登录
- 控制台侧:新增 WEB_PASSWORD 与 /api/login、/api/session,页面登录框验证通过才允许操作

大地图(src/core/bigMapService.js)
- 爬取服务器地图站点(squaremap)的瓦片图片与领土标记,一张图上看全部机器人坐标
- 坐标系对源站实测标定并用 27 个王城交叉验证;瓦片磁盘缓存,层级 0 启动预爬取、更高层级按需穿透
- 领土按 markers.json 原始多边形绘制(预编译 Path2D,even-odd 保留孔洞),叠加王城与机器人标签

配套改造
- TerritoryModel 新增 flyingChunksOf / ownChunksOf,邦国归属改为按名称显式传入,
  不再依赖全局 selfStateName(多机器人会互相覆盖)
- Web 接口以 /api/bots/:id/... 为主,原单机器人接口保留为默认机器人的等价别名
- 新增 scripts/check-web-ids.js 校验前端元素引用一致性

测试从 154 增至 242,全部通过。
2026-09-27 21:22:57 +08:00

152 行
6.2 KiB
JavaScript
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
'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 { BigMapService, TILE_PIXELS, DEFAULT_MAX_ZOOM } = require('../src/core/bigMapService');
const { defaultConfig } = require('../src/config/expansionConfig');
/** 造一张假的 PNG 内容(只需非空即可,服务端只做字节透传) */
function fakePng(size) {
const buffer = Buffer.alloc(size || 64);
buffer[0] = 0x89;
buffer[1] = 0x50;
return buffer;
}
/** 建一个使用临时缓存目录、带假 fetch 的大地图服务 */
function setup(options) {
const opts = options || {};
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'autostore-bigmap-'));
const config = defaultConfig();
Object.assign(config.bigMap, opts.bigMap || {});
const calls = [];
const fetchImpl = opts.fetchImpl || (async (url) => {
calls.push(url);
if (opts.failAll) return { ok: false, status: 404, arrayBuffer: async () => new ArrayBuffer(0) };
const data = fakePng(opts.tileSize || 64);
return { ok: true, status: 200, arrayBuffer: async () => data.buffer.slice(data.byteOffset, data.byteOffset + data.length) };
});
const service = new BigMapService(config, null, {
baseUrl: 'http://bgjq.simpfun.cn',
world: 'minecraft_overworld',
cacheDir,
fetchImpl
});
return { service, config, cacheDir, calls };
}
test('坐标系:最大层级 1 方块 = 1 像素,逐级翻倍', () => {
const { service } = setup();
assert.strictEqual(service.maxZoom, DEFAULT_MAX_ZOOM);
assert.strictEqual(service.blocksPerTile(3), TILE_PIXELS, '最大层级每张瓦片覆盖 512 方块');
assert.strictEqual(service.blocksPerTile(2), TILE_PIXELS * 2);
assert.strictEqual(service.blocksPerTile(1), TILE_PIXELS * 4);
assert.strictEqual(service.blocksPerTile(0), TILE_PIXELS * 8, '层级 0 每张瓦片覆盖 4096 方块');
});
test('坐标系:王城 (10722, 1547) 落在 z=3 的瓦片 (20, 3) 内(对源站实测标定)', () => {
const { service } = setup();
const bounds = service.tileBlockBounds(3, 20, 3);
assert.deepStrictEqual(bounds, { minBlockX: 10240, maxBlockX: 10752, minBlockZ: 1536, maxBlockZ: 2048 });
assert.ok(10722 >= bounds.minBlockX && 10722 < bounds.maxBlockX);
assert.ok(1547 >= bounds.minBlockZ && 1547 < bounds.maxBlockZ);
});
test('tileRange 覆盖 ±rangeBlocks:层级 0 全世界为 10×10=100 张', () => {
const { service } = setup();
const range = service.tileRange(0, 20480);
assert.deepStrictEqual(range, { minX: -5, maxX: 4, minZ: -5, maxZ: 4, count: 100 });
// 瓦片范围必须完整覆盖 -20480..20480
assert.strictEqual(range.minX * service.blocksPerTile(0), -20480);
assert.strictEqual((range.maxX + 1) * service.blocksPerTile(0), 20480);
});
test('tileUrl / tilePath 拼接源站地址与磁盘缓存路径', () => {
const { service } = setup();
assert.strictEqual(
service.tileUrl(2, -3, 7),
'http://bgjq.simpfun.cn/tiles/minecraft_overworld/2/-3_7.png'
);
assert.ok(service.tilePath(2, -3, 7).endsWith(path.join('minecraft_overworld', '2', '-3_7.png')));
});
test('getTile 首次回源并落盘,第二次命中磁盘不再回源', async () => {
const { service, calls } = setup();
const first = await service.getTile(1, 2, 3);
assert.ok(first && first.length > 0);
assert.strictEqual(calls.length, 1);
// 清掉内存缓存,验证磁盘缓存生效
service._memory.clear();
const second = await service.getTile(1, 2, 3);
assert.ok(second && second.length > 0);
assert.strictEqual(calls.length, 1, '第二次不应再回源');
});
test('getTile 回源失败返回 null(控制台画占位色块)', async () => {
const { service } = setup({ failAll: true });
assert.strictEqual(await service.getTile(0, 0, 0), null);
});
test('crawlAll 批量爬取范围内全部瓦片并统计进度', async () => {
const { service, calls } = setup({ bigMap: { rangeBlocks: 4096 } });
const result = await service.crawlAll(0);
// rangeBlocks=4096、层级 0 每张 4096 方块 → 2×2=4 张
assert.strictEqual(result.total, 4);
assert.strictEqual(result.done, 4);
assert.strictEqual(result.failed, 0);
assert.strictEqual(result.running, false);
assert.strictEqual(calls.length, 4);
assert.strictEqual(service.countCached(0), 4);
});
test('crawlAll 已缓存的瓦片不重复下载', async () => {
const { service, calls } = setup({ bigMap: { rangeBlocks: 4096 } });
await service.crawlAll(0);
assert.strictEqual(calls.length, 4);
await service.crawlAll(0);
assert.strictEqual(calls.length, 4, '第二次爬取应全部命中缓存');
});
test('crawlAll 统计失败张数', async () => {
const { service } = setup({ failAll: true, bigMap: { rangeBlocks: 4096 } });
const result = await service.crawlAll(0);
assert.strictEqual(result.total, 4);
assert.strictEqual(result.done, 0);
assert.strictEqual(result.failed, 4);
});
test('snapshot 暴露坐标系、瓦片范围与缓存数量', () => {
const { service } = setup({ bigMap: { rangeBlocks: 20480, zoom: 0 } });
const snap = service.snapshot();
assert.strictEqual(snap.world, 'minecraft_overworld');
assert.strictEqual(snap.tilePixels, TILE_PIXELS);
assert.strictEqual(snap.blocksPerTile, 4096);
assert.strictEqual(snap.tiles.count, 100);
assert.strictEqual(snap.cached, 0);
assert.ok(snap.source.tileUrlTemplate.includes('{z}/{x}_{y}.png'));
});
test('clearCache 清空磁盘与内存缓存', async () => {
const { service } = setup({ bigMap: { rangeBlocks: 4096 } });
await service.crawlAll(0);
assert.strictEqual(service.countCached(0), 4);
const removed = service.clearCache();
assert.strictEqual(removed, 4);
assert.strictEqual(service.countCached(0), 0);
assert.strictEqual(service._memory.size, 0);
});
test('配置里 zoom 大于 maxZoom 时被钳制(避免爬到源站不存在的瓦片)', () => {
const { service, config } = setup({ bigMap: { zoom: 7, maxZoom: 3 } });
// 直接走一次配置归一化,验证钳制逻辑
const { normalizeConfig } = require('../src/config/expansionConfig');
const normalized = normalizeConfig(config);
assert.strictEqual(normalized.bigMap.zoom, 3);
assert.strictEqual(service.maxZoom, 3);
});