This commit is contained in:
s
2026-06-30 10:56:41 +02:00
parent d49189c6e3
commit 1a5afcfd6d
11 changed files with 7927 additions and 0 deletions

101
server.js Normal file
View File

@@ -0,0 +1,101 @@
'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}`);
});