space / server.js
Skydata001's picture
Update server.js
e46c685 verified
Raw
History Blame Contribute Delete
8.97 kB
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}`);
});