File size: 5,180 Bytes
ee826ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/**
 * 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; }