'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} */ 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 };