/** * minecraft.js — Monitor del servidor MC * v2.9 —: cache de 30s en fetchStatus para evitar conexiones TCP masivas */ import net from 'net'; import { readConfig } from './utils.js'; const config = readConfig(); let lastStatus = { online: false, players: 0, max: 0, version: '', motd: '', sample: [], failCount: 0 }; let monitorTimer = null; // ── Cache de 30s para fetchStatus ───────────────────────────────────────────── // FIX: sin esto, 10 requests simultáneas en tools.js hacen 10 conexiones TCP al MC let _statusCache = null; let _statusCacheTime = 0; const STATUS_CACHE_TTL = 30_000; // 30 segundos function encodeVI(v) { const b = []; let val = v >>> 0; do { let byte = val & 0x7F; val >>>= 7; if (val) byte |= 0x80; b.push(byte); } while (val); return Buffer.from(b); } function decodeVI(buf, off) { let r = 0, sh = 0, byte, pos = off; do { byte = buf[pos++]; r |= (byte & 0x7F) << sh; sh += 7; } while (byte & 0x80); return { value: r, next: pos }; } function stripColor(s) { return typeof s === 'string' ? s.replace(/\u00a7./g,'').trim() : ''; } function motdStr(d) { if (!d) return ''; if (typeof d === 'string') return d; const t = d.text || (d.translate) || ''; const extra = d.extra ? d.extra.map(e => e.text||'').join('') : ''; return t + extra; } export function fetchServerStatus(host, port, timeoutMs) { if (!timeoutMs) timeoutMs = 8000; return new Promise(resolve => { const sock = new net.Socket(); let buf = Buffer.alloc(0), done = false; const fail = () => { if(done)return; done=true; sock.destroy(); resolve({online:false,players:0,max:0,version:'',motd:'',sample:[]}); }; const succeed = obj => { if(done)return; done=true; sock.destroy(); resolve(Object.assign({online:true},obj)); }; sock.setTimeout(timeoutMs); sock.on('timeout', fail); sock.on('error', fail); sock.on('close', ()=>{ if(!done)fail(); }); sock.connect(port, host, () => { const hb = Buffer.from(host,'utf8'); const payload = Buffer.concat([encodeVI(0),encodeVI(763),encodeVI(hb.length),hb,Buffer.from([port>>8,port&0xFF]),encodeVI(1)]); sock.write(Buffer.concat([encodeVI(payload.length),payload])); sock.write(Buffer.from([1,0])); }); sock.on('data', chunk => { buf = Buffer.concat([buf,chunk]); try { const plen = decodeVI(buf,0); if (buf.length < plen.next + plen.value) return; const pid = decodeVI(buf, plen.next); if (pid.value !== 0) return; const slen = decodeVI(buf, pid.next); const j = JSON.parse(buf.slice(slen.next, slen.next+slen.value).toString('utf8')); succeed({ players: j.players?.online||0, max: j.players?.max||0, version: j.version?.name||'', motd: stripColor(motdStr(j.description)), sample: (j.players?.sample||[]).map(p=>p.name), }); } catch(_) {} }); }); } function tcpPing(host, port, t) { return new Promise(resolve => { const s = new net.Socket(); let d = false; const fin = r => { if(d)return; d=true; s.destroy(); resolve(r); }; s.setTimeout(t||5000); s.on('connect',()=>fin(true)); s.on('timeout',()=>fin(false)); s.on('error',()=>fin(false)); try { s.connect(port, host); } catch(_) { fin(false); } }); } export async function fetchStatus() { // FIX: devolver cache si es reciente if (_statusCache && Date.now() - _statusCacheTime < STATUS_CACHE_TTL) { return _statusCache; } const { ip, port } = config.server; try { const s = await fetchServerStatus(ip, port, 8000); const result = s.online ? s : { online: await tcpPing(ip, port, 5000), players: 0, max: 0, version: '', motd: '', sample: [], }; _statusCache = result; _statusCacheTime = Date.now(); return result; } catch(_) { const result = { online: false, players: 0, max: 0, version: '', motd: '', sample: [] }; _statusCache = result; _statusCacheTime = Date.now(); return result; } } export function startMinecraftMonitor(onOffline, onOnline) { const { ip, port, checkInterval } = config.server; console.log(`[MC] Monitor ${ip}:${port}`); async function check() { // Invalidar cache para que el monitor siempre compruebe de verdad _statusCache = null; const was = lastStatus.online; const s = await fetchStatus().catch(()=>({ online: false })); const fc = s.online ? 0 : (lastStatus.failCount||0)+1; lastStatus = { ...s, failCount: fc }; if (!s.online && fc >= 3 && was) { console.warn('[MC] OFFLINE'); if (onOffline) await onOffline().catch(()=>{}); } else if (s.online && !was) { console.log(`[MC] ONLINE ${s.players}/${s.max}`); if (onOnline) await onOnline().catch(()=>{}); } else if (s.online) { console.log(`[MC] ${s.players}/${s.max} jugadores`); } } setTimeout(check, 10000); monitorTimer = setInterval(check, checkInterval); } export function stopMinecraftMonitor() { if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; } } export function getServerStatus() { return lastStatus; }