Spaces:
Sleeping
Sleeping
| import uWS from 'uWebSockets.js'; | |
| import { Packr } from 'msgpackr'; | |
| import { createClient } from '@libsql/client'; | |
| import dotenv from 'dotenv'; | |
| import fs from 'fs'; | |
| dotenv.config(); | |
| // إعداد ضغط البيانات (MessagePack) | |
| const packr = new Packr(); | |
| // إعداد قاعدة بيانات Turso (احتياطياً للمستقبل) | |
| const db = createClient({ | |
| url: process.env.TURSO_URL || 'file:local.db', | |
| authToken: process.env.TURSO_AUTH_TOKEN || '' | |
| }); | |
| const MAX_PLAYERS_PER_WORLD = 30; | |
| const DISCONNECT_GRACE_PERIOD = 60 * 1000; | |
| // خوارزمية توليد قارات بكسلية (Cellular Automata) | |
| function generatePixelMap(width, height) { | |
| const grid = new Uint8Array(width * height); | |
| // عشوائية أولية | |
| for (let i = 0; i < grid.length; i++) { | |
| grid[i] = Math.random() > 0.55 ? 1 : 0; // 1 يابسة، 0 ماء | |
| } | |
| // تنعيم لإنشاء قارات | |
| for (let iter = 0; iter < 4; iter++) { | |
| const newGrid = new Uint8Array(width * height); | |
| for (let y = 0; y < height; y++) { | |
| for (let x = 0; x < width; x++) { | |
| let neighbors = 0; | |
| for (let dy = -1; dy <= 1; dy++) { | |
| for (let dx = -1; dx <= 1; dx++) { | |
| if (dx === 0 && dy === 0) continue; | |
| let nx = x + dx, ny = y + dy; | |
| if (nx >= 0 && nx < width && ny >= 0 && ny < height) { | |
| neighbors += grid[ny * width + nx]; | |
| } | |
| } | |
| } | |
| // قواعد الحياة للقارات | |
| newGrid[y * width + x] = neighbors > 4 || (neighbors === 4 && grid[y * width + x]) ? 1 : 0; | |
| } | |
| } | |
| for (let i = 0; i < grid.length; i++) grid[i] = newGrid[i]; | |
| } | |
| return grid; | |
| } | |
| class World { | |
| constructor(id) { | |
| this.id = id; | |
| this.players = new Map(); | |
| // إعداد الخريطة البكسلية (مثلاً 250x250 بكسل/مربع) | |
| this.mapWidth = 250; | |
| this.mapHeight = 250; | |
| this.mapGrid = generatePixelMap(this.mapWidth, this.mapHeight); | |
| } | |
| get playerCount() { | |
| let count = 0; | |
| this.players.forEach(p => { if (p.online) count++; }); | |
| return count; | |
| } | |
| addPlayer(ws, playerData) { | |
| this.players.set(ws.id, playerData); | |
| } | |
| removePlayer(wsId) { | |
| this.players.delete(wsId); | |
| } | |
| } | |
| // إنشاء 5 عوالم | |
| const worlds = [1, 2, 3, 4, 5].map(id => new World(id)); | |
| const app = uWS.App().ws('/*', { | |
| compression: uWS.SHARED_COMPRESSOR, | |
| maxPayloadLength: 16 * 1024 * 1024, | |
| idleTimeout: 60, | |
| upgrade: (res, req, context) => { | |
| const ip = res.getRemoteAddressAsText(); | |
| res.upgrade( | |
| { id: Math.random().toString(36).substr(2, 9), ip }, | |
| req.getHeader('sec-websocket-key'), | |
| req.getHeader('sec-websocket-protocol'), | |
| req.getHeader('sec-websocket-extensions'), | |
| context | |
| ); | |
| }, | |
| open: (ws) => { | |
| ws.isAlive = true; | |
| const worldStats = worlds.map(w => ({ id: w.id, players: w.playerCount })); | |
| ws.send(packr.pack({ type: 'SERVER_LIST', data: worldStats }), true); | |
| }, | |
| message: (ws, message, isBinary) => { | |
| try { | |
| if (!isBinary) return; | |
| const data = packr.unpack(message); | |
| switch (data.type) { | |
| case 'JOIN_WORLD': | |
| handleJoinWorld(ws, data.worldId, data.playerName); | |
| break; | |
| case 'PING': | |
| ws.send(packr.pack({ type: 'PONG' }), true); | |
| break; | |
| case 'REFRESH_SERVERS': | |
| const stats = worlds.map(w => ({ id: w.id, players: w.playerCount })); | |
| ws.send(packr.pack({ type: 'SERVER_LIST', data: stats }), true); | |
| break; | |
| } | |
| } catch (e) { | |
| // تجاهل البيانات الخبيثة | |
| } | |
| }, | |
| close: (ws, code, message) => { | |
| if (ws.worldId) { | |
| const world = worlds.find(w => w.id === ws.worldId); | |
| if (world && world.players.has(ws.id)) { | |
| const player = world.players.get(ws.id); | |
| player.online = false; | |
| setTimeout(() => { | |
| const checkPlayer = world.players.get(ws.id); | |
| if (checkPlayer && !checkPlayer.online) { | |
| world.removePlayer(ws.id); | |
| broadcast(world, { type: 'PLAYER_REMOVED', id: ws.id }); | |
| } | |
| }, DISCONNECT_GRACE_PERIOD); | |
| } | |
| } | |
| } | |
| }); | |
| function handleJoinWorld(ws, worldId, playerName) { | |
| const world = worlds.find(w => w.id === worldId); | |
| if (!world) return; | |
| if (world.playerCount >= MAX_PLAYERS_PER_WORLD) { | |
| ws.send(packr.pack({ type: 'ERROR', msg: 'الخادم ممتلئ' }), true); | |
| return; | |
| } | |
| const safeName = String(playerName).substring(0, 20).replace(/[^a-zA-Z0-9أ-ي]/g, ''); | |
| ws.worldId = worldId; | |
| ws.subscribe(`world-${worldId}`); | |
| const color = `#${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')}`; | |
| world.addPlayer(ws, { | |
| id: ws.id, | |
| name: safeName, | |
| color: color, | |
| online: true | |
| }); | |
| ws.send(packr.pack({ type: 'JOIN_SUCCESS', worldId, color, name: safeName }), true); | |
| // إرسال الخريطة البكسلية للعميل (عرض، طول، والمصفوفة) | |
| ws.send(packr.pack({ | |
| type: 'MAP_DATA', | |
| width: world.mapWidth, | |
| height: world.mapHeight, | |
| grid: world.mapGrid | |
| }), true); | |
| broadcast(world, { type: 'PLAYER_JOINED', id: ws.id, name: safeName }); | |
| } | |
| function broadcast(world, messageObj) { | |
| const buffer = packr.pack(messageObj); | |
| app.publish(`world-${world.id}`, buffer, true); | |
| } | |
| // تقديم ملف الواجهة (index.html) | |
| app.get('/*', (res, req) => { | |
| try { | |
| const html = fs.readFileSync('index.html', 'utf8'); | |
| res.writeHeader('Content-Type', 'text/html; charset=utf-8').end(html); | |
| } catch (e) { | |
| res.writeStatus('500 Internal Server Error').end('Index file not found'); | |
| } | |
| }); | |
| const PORT = process.env.PORT || 7860; | |
| app.listen(PORT, (token) => { | |
| if (token) { | |
| console.log(`Server running on port ${PORT} (Pixel Grid Engine Ready)`); | |
| } else { | |
| console.log(`Failed to listen to port ${PORT}`); | |
| } | |
| }); |