Spaces:
Paused
Paused
File size: 8,972 Bytes
79c347f a5bdf77 77d1510 e46c685 77d1510 79c347f a5bdf77 79c347f 61c0e82 a5bdf77 61c0e82 e46c685 79c347f a5bdf77 00580ef a5bdf77 79c347f a5bdf77 79c347f a5bdf77 e46c685 79c347f a5bdf77 e46c685 a5bdf77 79c347f a5bdf77 77d1510 e46c685 a5bdf77 e46c685 a5bdf77 77d1510 a5bdf77 e46c685 a5bdf77 e46c685 a5bdf77 e46c685 a5bdf77 e46c685 77d1510 a5bdf77 77d1510 a5bdf77 e46c685 a5bdf77 e46c685 a5bdf77 e46c685 77d1510 a5bdf77 77d1510 e46c685 a5bdf77 79c347f a5bdf77 79c347f | 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 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | 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}`);
}); |