'use strict'; // Serveur de sync communautaire Farm Tracker. // Stocke les stats de chaque joueur (identifié par UUID) et renvoie l'agrégat. // Lance avec : node server.js (ou PORT=3742 node server.js) // Aucune dépendance externe. const http = require('http'); const fs = require('fs'); const path = require('path'); const DATA_FILE = path.join(__dirname, 'sync-data.json'); const PORT = Number(process.env.PORT) || 3742; const MAX_BODY = 10 * 1024 * 1024; // 10 Mo max par requête function loadData() { try { return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); } catch { return { clients: {} }; } } function saveData(data) { const tmp = DATA_FILE + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(data)); fs.renameSync(tmp, DATA_FILE); } function mergeInto(target, source) { for (const [mob, mobData] of Object.entries(source.mobs || {})) { if (!mob) continue; if (!target.mobs[mob]) target.mobs[mob] = { kills: 0, items: {} }; const t = target.mobs[mob]; t.kills += Number(mobData.kills) || 0; for (const [key, item] of Object.entries(mobData.items || {})) { if (!t.items[key]) t.items[key] = { rawName: '', name: '', occurrences: 0, totalQty: 0 }; t.items[key].occurrences += Number(item.occurrences) || 0; t.items[key].totalQty += Number(item.totalQty) || 0; if (item.rawName) t.items[key].rawName = item.rawName; if (item.name) t.items[key].name = item.name; } } } // Recompute l'agrégat à partir des contributions individuelles de chaque client. // Chaque client est compté exactement une fois (la dernière soumission). function computeAggregate(clients) { const agg = { type: 'farm-tracker-dropstats', version: 1, updatedAt: new Date().toISOString(), mobs: {} }; for (const stats of Object.values(clients)) mergeInto(agg, stats); return agg; } http.createServer((req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } if (req.method === 'GET' && req.url === '/stats') { const data = loadData(); const aggregate = computeAggregate(data.clients); const clientCount = Object.keys(data.clients).length; res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, dropStats: aggregate, clientCount })); return; } if (req.method !== 'POST' || req.url !== '/sync') { res.writeHead(404); res.end('Not found'); return; } let body = ''; req.on('data', chunk => { body += chunk; if (body.length > MAX_BODY) req.destroy(); }); req.on('end', () => { try { const { clientId, dropStats } = JSON.parse(body); if (!clientId || typeof clientId !== 'string' || clientId.length > 64) throw new Error('clientId invalide'); if (!dropStats || dropStats.type !== 'farm-tracker-dropstats') throw new Error('Format dropStats invalide'); const data = loadData(); data.clients[clientId] = dropStats; const aggregate = computeAggregate(data.clients); saveData(data); const clientCount = Object.keys(data.clients).length; console.log(`[${new Date().toISOString()}] sync client=${clientId.slice(0, 8)}… clients=${clientCount}`); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, dropStats: aggregate, clientCount })); } catch (e) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: false, error: e.message })); } }); req.on('error', () => {}); }).listen(PORT, '0.0.0.0', () => { console.log(`Farm Tracker sync server — port ${PORT} — data: ${DATA_FILE}`); });