const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const path = require('path'); const http = require('http'); const WebSocket = require('ws'); const { v4: uuidv4 } = require('uuid'); // ========================================== // 🛡️ TURSO DATABASE (PASSIVE CONNECTION) // ========================================== let db = null; try { const { createClient } = require('@libsql/client'); db = createClient({ url: process.env.TURSO_URL || 'libsql://skydata-admin3388-6.turso.io', authToken: process.env.TURSO_AUTH_TOKEN || '' }); console.log('[DB] Turso client initialized (passive mode).'); } catch (e) { console.warn('[DB] Turso client failed to load, continuing without DB:', e.message); } async function initDB() { try { if (!db) { console.log('[DB] No DB client available, skipping connection test.'); return; } await db.execute("SELECT 1"); console.log('[DB] Turso connection verified (SELECT 1 OK).'); } catch (e) { console.warn('[DB] Turso connection test failed, server will continue in RAM-only mode:', e.message); } } initDB(); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); const PORT = 7860; app.set('trust proxy', 1); app.use(helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })); const allowedOrigins = ['http://localhost:3000', 'https://agenuclear.gamer.gd', 'https://skydata001-space.hf.space']; app.use(cors({ origin: function(origin, callback) { if (!origin || allowedOrigins.includes(origin)) callback(null, true); else callback(new Error('Unauthorized')); }, exposedHeaders: ['Content-Length', 'ETag', 'Last-Modified'] })); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 1000, standardHeaders: true, legacyHeaders: false }); app.use(limiter); app.use('/', express.static(path.join(__dirname, 'public'), { setHeaders: (res) => res.set('Cache-Control', 'public, max-age=86400') })); // ========================================== // 🎮 MULTIPLAYER GAME STATE (RAM ONLY) // ========================================== const rooms = new Map(); const pvpQueue = []; function createRoom(mode, maxPlayers) { const roomId = uuidv4(); rooms.set(roomId, { id: roomId, mode: mode, maxPlayers: maxPlayers, players: new Map(), // ws -> { joinedAt, token } claims: new Map(), // iso -> { name, color } status: 'waiting' }); return roomId; } // ========================================== // 🚀 WEBSOCKET HANDLER // ========================================== wss.on('connection', (ws, req) => { // Security: Check Origin const origin = req.headers.origin; if (origin && !allowedOrigins.includes(origin)) { ws.close(1008, 'Unauthorized Protocol'); return; } let currentRoomId = null; let isClaimed = false; let claimTimer = null; ws.on('message', (message) => { try { const data = JSON.parse(message); // 1. JOIN MATCHMAKING (LOBBY) — فقط يحجز غرفة ويرسل معلوماتها if (data.type === 'join_matchmaking') { if (data.mode === 'ww') { let targetRoom = [...rooms.values()].find(r => r.mode === 'ww' && r.players.size < r.maxPlayers); if (!targetRoom) { const newRoomId = createRoom('ww', 50); targetRoom = rooms.get(newRoomId); } const matchToken = uuidv4(); ws.send(JSON.stringify({ type: 'match_found', roomId: targetRoom.id, token: matchToken })); } else if (data.mode === 'pvp') { pvpQueue.push(ws); updatePvPQueue(); if (pvpQueue.length >= 2) { const p1 = pvpQueue.shift(); const p2 = pvpQueue.shift(); const roomId = createRoom('pvp', 2); const room = rooms.get(roomId); const token1 = uuidv4(); const token2 = uuidv4(); if (p1.readyState === WebSocket.OPEN) p1.send(JSON.stringify({ type: 'match_found', roomId: room.id, token: token1 })); if (p2.readyState === WebSocket.OPEN) p2.send(JSON.stringify({ type: 'match_found', roomId: room.id, token: token2 })); } } } // 2. JOIN ROOM — اللاعب يدخل الجولة الفعلية من game.html if (data.type === 'join_room') { const room = rooms.get(data.roomId); if (room && room.players.size < room.maxPlayers) { currentRoomId = data.roomId; room.players.set(ws, { joinedAt: Date.now(), token: data.token }); ws.send(JSON.stringify({ type: 'joined_room', roomId: room.id })); // إرسال الدول المحتلة الحالية const existingClaims = Array.from(room.claims.entries()).map(([iso, info]) => ({ iso, name: info.name, color: info.color })); ws.send(JSON.stringify({ type: 'room_state', claims: existingClaims })); // مؤقت 120 ثانية claimTimer = setTimeout(() => { if (!isClaimed) { ws.send(JSON.stringify({ type: 'kick', msg: 'انتهى الوقت (120 ثانية) ولم تختر دولة.' })); ws.close(1000, 'Time Out'); } }, 120000); } else { ws.send(JSON.stringify({ type: 'error', msg: 'الغرفة ممتلئة أو غير موجودة!' })); } } // 3. CLAIM COUNTRY if (data.type === 'claim_country') { if (!currentRoomId) { ws.send(JSON.stringify({ type: 'error', msg: 'أنت لست في جولة! أعد الدخول من اللوبي.' })); return; } const room = rooms.get(currentRoomId); if (!room) { ws.send(JSON.stringify({ type: 'error', msg: 'الغرفة غير موجودة!' })); return; } const { iso, name, color } = data; if (isClaimed) return ws.send(JSON.stringify({ type: 'error', msg: 'أنت تملك دولة بالفعل!' })); if (room.claims.has(iso)) return ws.send(JSON.stringify({ type: 'error', msg: 'هذه الدولة محتلة بالفعل!' })); const nameTaken = Array.from(room.claims.values()).some(c => c.name === name); if (nameTaken) return ws.send(JSON.stringify({ type: 'error', msg: 'هذا الاسم مستخدم في هذه الجولة!' })); if (!/^[A-Za-z]{3,10}$/.test(name)) return ws.send(JSON.stringify({ type: 'error', msg: 'الاسم غير صالح!' })); room.claims.set(iso, { name, color }); isClaimed = true; clearTimeout(claimTimer); const claimData = { type: 'player_claimed', iso, name, color }; room.players.forEach((playerData, playerWs) => { if (playerWs.readyState === WebSocket.OPEN) { playerWs.send(JSON.stringify(claimData)); } }); ws.send(JSON.stringify({ type: 'claim_success', iso, name, color })); } } catch (e) { console.warn('[WS] Malformed Message Detected'); } }); function updatePvPQueue() { pvpQueue.forEach((qWs, index) => { if (qWs.readyState === WebSocket.OPEN) { qWs.send(JSON.stringify({ type: 'queue_update', pos: index + 1, total: pvpQueue.length })); } }); } ws.on('close', () => { const qIndex = pvpQueue.indexOf(ws); if (qIndex > -1) { pvpQueue.splice(qIndex, 1); updatePvPQueue(); } if (currentRoomId) { const room = rooms.get(currentRoomId); if (room) { room.players.delete(ws); if (room.players.size === 0) { console.log(`[RAM] Room ${room.id} ended. Claims: ${room.claims.size}. Data discarded from RAM.`); rooms.delete(currentRoomId); } } } }); }); server.listen(PORT, '0.0.0.0', () => { console.log(`[Multiplayer] Server running on port ${PORT}`); });