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
加载中 0%
点击按钮进入全屏沉浸式实验环境
`;
// 注入到 之前的 JS(不修改原始 Unity 加载逻辑)
const js = `
`;
html = html.replace(
/progressBarFull\.style\.width\s*=\s*100\s*\*\s*progress\s*\+\s*"%";/,
'progressBarFull.style.width = 100 * progress + "%"; if (window.__landingSetProgress) window.__landingSetProgress(progress);'
);
if (!html.includes('name="viewport"')) {
html = html.replace(
'',
'\\n '
);
}
html = html.replace(
/