zelin-bot / src /minecraft.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* 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; }