// --------------------------------------------------------------- // Dino Isle Online — main server: static files + websocket + game loop // Run: node server.js (PORT env optional, default 8090) // --------------------------------------------------------------- 'use strict'; const http = require('http'); const fs = require('fs'); const path = require('path'); const { Game, TICK_MS } = require('./sim'); const wsattach = require('./ws').attach; const PORT = parseInt(process.env.PORT || '8095', 10); const PUBLIC_DIR = path.join(__dirname, 'public'); const CHEATS = process.env.DEBUG_CHEATS === '1'; const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff2': 'font/woff2', }; const server = http.createServer((req, res) => { let urlPath = decodeURIComponent((req.url || '/').split('?')[0]); if (urlPath === '/') urlPath = '/index.html'; const filePath = path.normalize(path.join(PUBLIC_DIR, urlPath)); if (!filePath.startsWith(PUBLIC_DIR)) { res.writeHead(403); res.end('Forbidden'); return; } fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not found'); return; } const ext = path.extname(filePath).toLowerCase(); res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream', 'Cache-Control': 'no-store', }); res.end(data); }); }); const game = new Game(); let nextAllowedMsg = new WeakMap(); // conn -> timestamp budget wsattach(server, '/ws', (conn) => { conn.onmessage = (str) => { let m; try { m = JSON.parse(str); } catch (_) { return; } if (!m || typeof m !== 'object') return; // simple flood guard: max ~40 msgs/sec const now = Date.now(); const budget = nextAllowedMsg.get(conn) || now; if (now < budget - 1000) return; // way over -> drop nextAllowedMsg.set(conn, Math.max(budget + 25, now)); const p = game.players.get(conn); switch (m.t) { case 'join': { const player = game.join(conn, String(m.name || '').slice(0, 16), m.sp); conn.send(JSON.stringify(game.welcomePayload(player))); break; } case 'in': if (p) game.onInput(p, m); break; case 'chat': if (p && p.alive !== undefined) game.onChat(p, m.msg); break; case 'dev': if (p) game.onDev(p, m); break; case 'devact': if (p) game.onDevAct(p, String(m.act || '')); break; case 'ping': conn.send('{"t":"pong"}'); break; case 'tp': if (p && (CHEATS || p.dev)) { p.x = Math.max(40, Math.min(7680 - 40, +m.x || p.x)); p.y = Math.max(40, Math.min(7680 - 40, +m.y || p.y)); if (typeof m.a === 'number') p.dir = m.a; p.vx = 0; p.vy = 0; } break; default: break; } }; conn.onclose = () => game.leave(conn); }); // heartbeat setInterval(() => { for (const p of [...game.players.keys()]) { const c = p; // key is conn if (c.writable) c.ping(); } }, 25000); // main loop with drift compensation let last = Date.now(); setInterval(() => { const now = Date.now(); let dt = (now - last) / 1000; last = now; dt = Math.min(dt, 0.25); // clamp big pauses try { game.tick(dt); } catch (e) { console.error('tick error', e); } }, TICK_MS); server.listen(PORT, () => { console.log(`[dino-isle] listening on http://localhost:${PORT} (seed=${game.seed})`); console.log(`[dino-isle] open multiple browser tabs for multiplayer fun!`); });