const http = require('http'); const fs = require('fs'); const path = require('path'); const PORT = process.env.PORT || 24710; const HOST = process.env.HOST || '0.0.0.0'; const ROOT = __dirname; const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript', '.wasm': 'application/wasm', '.data': 'application/octet-stream', '.css': 'text/css', '.png': 'image/png', '.ico': 'image/x-icon', '.svg': 'image/svg+xml', '.json': 'application/json', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.mp4': 'video/mp4', '.m4v': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg', }; function extType(p) { return MIME[path.extname(p).toLowerCase()] || 'application/octet-stream'; } function originalCompressedPath(fp) { if (fp.endsWith('.unityweb')) return fp.slice(0, -'.unityweb'.length); if (fp.endsWith('.gz')) return fp.slice(0, -'.gz'.length); if (fp.endsWith('.br')) return fp.slice(0, -'.br'.length); return fp; } function headersForFile(fp, size, encoding) { const originalPath = originalCompressedPath(fp); const fileName = path.basename(originalPath); const isHashedBuildAsset = /\/Build\/[a-f0-9]{32}\./.test(fp); const headers = { 'Content-Type': extType(originalPath), 'Content-Length': size, 'X-Content-Type-Options': 'nosniff', 'Accept-Ranges': 'bytes', }; if (encoding != null) { headers['Content-Encoding'] = encoding; headers['Cache-Control'] = isHashedBuildAsset ? 'public, max-age=31536000, immutable' : 'public, max-age=604800'; } else if (/\.(mp4|m4v|webm|ogg)$/i.test(fp)) { headers['Cache-Control'] = 'public, max-age=31536000, immutable'; } else if (fileName === 'ServiceWorker.js' || fileName === 'index.html' || fileName === 'manifest.webmanifest') { headers['Cache-Control'] = 'no-cache'; } else if (isHashedBuildAsset) { headers['Cache-Control'] = 'public, max-age=31536000, immutable'; } else { headers['Cache-Control'] = 'public, max-age=3600'; } return headers; } function addValidators(headers, stat) { headers['ETag'] = `"${stat.size.toString(16)}-${Math.floor(stat.mtimeMs).toString(16)}"`; headers['Last-Modified'] = stat.mtime.toUTCString(); return headers; } function isFresh(req, headers) { const ifNoneMatch = req.headers['if-none-match']; if (ifNoneMatch && ifNoneMatch === headers['ETag']) return true; const ifModifiedSince = req.headers['if-modified-since']; if (ifModifiedSince && headers['Last-Modified']) { return new Date(ifModifiedSince).getTime() >= new Date(headers['Last-Modified']).getTime(); } return false; } function detectUnityEncoding(fp, callback) { if (fp.endsWith('.gz')) { callback(null, 'gzip'); return; } if (fp.endsWith('.br')) { callback(null, 'br'); return; } if (!fp.endsWith('.unityweb')) { callback(null, null); return; } fs.open(fp, 'r', (openErr, fd) => { if (openErr) { callback(openErr); return; } const buf = Buffer.alloc(2); fs.read(fd, buf, 0, 2, 0, (readErr) => { fs.close(fd, () => {}); if (readErr) { callback(readErr); return; } callback(null, buf[0] === 0x1f && buf[1] === 0x8b ? 'gzip' : 'br'); }); }); } // 首页:基于原始 index.html,注入启动封面(不修改任何 Unity 加载逻辑) function serveIndex(res) { try { let html = fs.readFileSync(path.join(ROOT, 'index.html'), 'utf-8'); // 注入到 之前的 CSS const css = ` `; // 注入到
最前面的 overlay const overlay = `VIRTUAL SIMULATION EXPERIMENT
点击按钮进入全屏沉浸式实验环境