Spaces:
Paused
Paused
| 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}`); | |
| }); |