按 TODO.md 的优先级把待办全部落地为代码,详见 CHANGELOG 的 [1.7.0]。 - 「走不动」攻关:地面状态修正(syncGroundToPhysics / trustSetbackOnGround)、 出站包类型分布、备选执行器 manualMover(只借 A* 规划、自己按键执行) - 原点自救分两轮并统计成败;回城前评估「原地是否更差」并记录前后坐标 - 新增目标区块租约表(targetLease)避免多机器人撞同一块 - 定时消息支持 cron 与「期望回复」确认;新增人工介入 Webhook 通知(notifier) - 大地图后台预热与视野裁剪;控制台日志按帧批量写入、配置面板支持搜索 顺带修掉两个影响部署的细节: - 部署脚本(deploy.js / ssh-helper.js)改为也能从 .env 读 SSH 凭据 —— 在命令行传密码会留在 shell 历史与进程列表里,写进已被忽略的 .env 更安全; 显式环境变量仍然优先。.env.example 补上占位符并说明这些只在本机用。 - package.json / package-lock.json 版本对齐到 1.7.0(此前停留在 1.1.0, 与 CHANGELOG 的语义化版本声明脱节)。 测试:368 → 436 个用例,`npm test` 与 `npm run check` 全绿。
160 行
5.1 KiB
JavaScript
160 行
5.1 KiB
JavaScript
'use strict';
|
||
|
||
const chunkPos = require('./chunkPos');
|
||
|
||
/**
|
||
* 目标区块租约表(多机器人协调用)。
|
||
*
|
||
* 背景(TODO.md 4.3):领土模型是全局共享的,但「谁要去占哪一块」只存在各机器人
|
||
* 自己的队列里,彼此看不见 —— 多个机器人同时扩地时很容易撞在同一批候选区块上:
|
||
* 两个机器人同时跑到同一区块、只有一个能开垦成功,另一半时间全白花。
|
||
*
|
||
* 这里给「目标区块」加一张带租约的占用表:
|
||
* - **抢占**:`tryAcquire` 是「检查 + 写入」的原子操作,谁先抢到谁去;
|
||
* - **租约**:占用有有效期(默认 3 分钟),持有者持续续租;机器人掉线 / 卡死时
|
||
* 租约到期自动失效,区块不会被永久锁死;
|
||
* - **释放**:开垦成功 / 放弃目标 / 停止任务时主动释放,其他机器人立刻可用。
|
||
*
|
||
* 同一 Node 进程内管理所有机器人,因此不需要真正的分布式锁,
|
||
* 一张内存表就足以覆盖「同进程多机器人」这个实际场景。
|
||
*/
|
||
|
||
/** 默认租约时长(毫秒) */
|
||
const DEFAULT_TTL_MS = 180000;
|
||
|
||
class TargetLeaseTable {
|
||
/**
|
||
* @param {{ttlMs?: number}} [options]
|
||
*/
|
||
constructor(options) {
|
||
const opts = options || {};
|
||
this.ttlMs = Number.isFinite(opts.ttlMs) && opts.ttlMs > 0 ? opts.ttlMs : DEFAULT_TTL_MS;
|
||
/** @type {Map<string, {botId: string, expiresAt: number, acquiredAt: number}>} */
|
||
this._leases = new Map();
|
||
}
|
||
|
||
/**
|
||
* 尝试占用一个区块(同一个机器人可重复占用 / 续租)。
|
||
*
|
||
* @param {{x: number, z: number}} chunk
|
||
* @param {string} botId
|
||
* @param {number} [ttlMs] 本次租约时长,缺省取表上的 ttlMs
|
||
* @returns {boolean} 是否占用成功(已被其他机器人占用时返回 false)
|
||
*/
|
||
tryAcquire(chunk, botId, ttlMs) {
|
||
if (!chunk) return false;
|
||
const key = chunkPos.key(chunk.x, chunk.z);
|
||
const now = Date.now();
|
||
const existing = this._leases.get(key);
|
||
if (existing && existing.expiresAt > now && existing.botId !== botId) return false;
|
||
const ttl = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : this.ttlMs;
|
||
this._leases.set(key, {
|
||
botId,
|
||
expiresAt: now + ttl,
|
||
acquiredAt: existing && existing.botId === botId ? existing.acquiredAt : now
|
||
});
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 续租(只有当前持有者能续)。
|
||
* @param {{x: number, z: number}} chunk
|
||
* @param {string} botId
|
||
* @param {number} [ttlMs]
|
||
* @returns {boolean} 是否续租成功
|
||
*/
|
||
renew(chunk, botId, ttlMs) {
|
||
if (!chunk) return false;
|
||
const key = chunkPos.key(chunk.x, chunk.z);
|
||
const existing = this._leases.get(key);
|
||
if (!existing || existing.botId !== botId) return false;
|
||
const ttl = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : this.ttlMs;
|
||
existing.expiresAt = Date.now() + ttl;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 释放占用(只有当前持有者能释放;已过期的占用直接清掉)。
|
||
* @param {{x: number, z: number}} chunk
|
||
* @param {string} botId
|
||
* @returns {boolean} 是否释放了一个占用
|
||
*/
|
||
release(chunk, botId) {
|
||
if (!chunk) return false;
|
||
const key = chunkPos.key(chunk.x, chunk.z);
|
||
const existing = this._leases.get(key);
|
||
if (!existing) return false;
|
||
if (existing.botId !== botId) return false;
|
||
this._leases.delete(key);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 释放某个机器人的全部占用(停止任务 / 断线 / 退出时调用)。
|
||
* @param {string} botId
|
||
* @returns {number} 释放的数量
|
||
*/
|
||
releaseAll(botId) {
|
||
let released = 0;
|
||
for (const [key, lease] of this._leases) {
|
||
if (lease.botId === botId) {
|
||
this._leases.delete(key);
|
||
released++;
|
||
}
|
||
}
|
||
return released;
|
||
}
|
||
|
||
/**
|
||
* 区块是否被**其他**机器人占用(已过期的占用视为空闲,顺手清理)。
|
||
* @param {{x: number, z: number}} chunk
|
||
* @param {string} botId 自己
|
||
* @returns {boolean}
|
||
*/
|
||
isLeasedByOther(chunk, botId) {
|
||
if (!chunk) return false;
|
||
const key = chunkPos.key(chunk.x, chunk.z);
|
||
const lease = this._leases.get(key);
|
||
if (!lease) return false;
|
||
if (lease.expiresAt <= Date.now()) {
|
||
this._leases.delete(key);
|
||
return false;
|
||
}
|
||
return lease.botId !== botId;
|
||
}
|
||
|
||
/**
|
||
* 清理过期租约。
|
||
* @returns {number} 清理掉的数量
|
||
*/
|
||
sweep() {
|
||
const now = Date.now();
|
||
let removed = 0;
|
||
for (const [key, lease] of this._leases) {
|
||
if (lease.expiresAt <= now) {
|
||
this._leases.delete(key);
|
||
removed++;
|
||
}
|
||
}
|
||
return removed;
|
||
}
|
||
|
||
/** @returns {number} 当前有效占用数量 */
|
||
get size() {
|
||
this.sweep();
|
||
return this._leases.size;
|
||
}
|
||
|
||
/** @returns {Array<{chunk: string, botId: string, remainingMs: number}>} 供控制台展示 */
|
||
snapshot() {
|
||
const now = Date.now();
|
||
const rows = [];
|
||
for (const [key, lease] of this._leases) {
|
||
if (lease.expiresAt <= now) continue;
|
||
rows.push({ chunk: key, botId: lease.botId, remainingMs: lease.expiresAt - now });
|
||
}
|
||
return rows;
|
||
}
|
||
}
|
||
|
||
module.exports = { TargetLeaseTable, DEFAULT_TTL_MS }; |