14 KiB
HTML Polyfills
The game-service automatically injects JavaScript polyfills into HTML files to improve compatibility with Unity WebGL and other game engines.
Overview
Many legacy games expect certain browser APIs and global functions to exist. The HTML Polyfill Injector detects game engines and automatically adds compatibility shims to prevent errors and improve stability.
Architecture
HTML File (from disk/ZIP/CDN)
↓
Check if HTML content
↓
Detect game engine (Unity, etc.)
↓
Build polyfill script
↓
Inject into <head> tag
↓
Return modified HTML
Polyfill Categories
1. Unity WebGL Polyfills
Injected when Unity game is detected.
Detection Patterns
function needsUnityPolyfills(html: string): boolean {
const unityIndicators = [
/UnityProgress/i, // Unity 5.x progress callback
/UnityLoader/i, // Unity loader script
/createUnityInstance/i, // Unity 2020+ API
/unityFramework\.js/i, // Unity framework
/Build\/.*\.loader\.js/i, // Unity build files
/UnityEngine/i, // Unity namespace
];
return unityIndicators.some((pattern) => pattern.test(html));
}
Any match triggers Unity polyfills injection
Unity Polyfills
// Unity WebGL Polyfills
// 1. UnityProgress - Progress callback for Unity 5.x
window.UnityProgress =
window.UnityProgress ||
function (gameInstance, progress) {
if (!gameInstance.Module) return;
if (!gameInstance.progress) {
gameInstance.progress = { loaded: 0, total: 1 };
}
gameInstance.progress.loaded = progress;
if (progress === 1) {
console.log('[Unity] Game loaded successfully');
}
};
// 2. createUnityInstance - Unity 2020+ loader API
window.createUnityInstance =
window.createUnityInstance ||
function (canvas, config) {
return new Promise((resolve) => {
console.log('[Unity] createUnityInstance called - using polyfill');
resolve({
Module: {},
SetFullscreen: function () {},
SendMessage: function () {},
Quit: function () {
return Promise.resolve();
},
});
});
};
// 3. UnityLoader2020 - Error handler
if (typeof UnityLoader2020 === 'undefined') {
window.UnityLoader2020 = {
Error: {
handler: function (message, filename, lineno) {
console.warn('[Unity] Error:', message, 'at', filename + ':' + lineno);
return true; // Prevent default error handling
},
},
};
}
2. General Compatibility Polyfills
Injected for all HTML files.
// General game compatibility polyfills
// 1. window.external - Legacy ActiveX shim
if (typeof window.external === 'undefined') {
window.external = {};
}
// 2. AudioContext - WebKit prefix polyfill
if (
typeof AudioContext === 'undefined' &&
typeof webkitAudioContext !== 'undefined'
) {
window.AudioContext = webkitAudioContext;
}
Injection Process
HTML Detection
export function injectPolyfills(html: Buffer | string): Buffer {
let htmlString = Buffer.isBuffer(html) ? html.toString('utf-8') : html;
// Check if this is actually HTML
if (!htmlString.includes('<html') && !htmlString.includes('<head')) {
// Not an HTML file, return as-is
return Buffer.from(htmlString);
}
// ... continue with injection
}
Non-HTML files are returned unchanged
Polyfill Selection
let polyfillsToInject = '';
// Add Unity polyfills if needed
if (needsUnityPolyfills(htmlString)) {
polyfillsToInject += UNITY_POLYFILLS;
console.log('[HTMLInjector] Injecting Unity WebGL polyfills');
}
// Always add general polyfills
polyfillsToInject += GENERAL_POLYFILLS;
Injection Strategy
// Try to inject into <head> tag
const headMatch = htmlString.match(/<head[^>]*>/i);
if (headMatch) {
const headOpenTag = headMatch[0];
const insertPosition = htmlString.indexOf(headOpenTag) + headOpenTag.length;
htmlString =
htmlString.slice(0, insertPosition) +
polyfillsToInject +
htmlString.slice(insertPosition);
} else {
// No <head> tag, try to inject after <html>
const htmlMatch = htmlString.match(/<html[^>]*>/i);
if (htmlMatch) {
const htmlOpenTag = htmlMatch[0];
const insertPosition = htmlString.indexOf(htmlOpenTag) + htmlOpenTag.length;
htmlString =
htmlString.slice(0, insertPosition) +
'<head>' +
polyfillsToInject +
'</head>' +
htmlString.slice(insertPosition);
} else {
// No <html> or <head> tag, wrap entire content
htmlString =
'<!DOCTYPE html><html><head>' +
polyfillsToInject +
'</head><body>' +
htmlString +
'</body></html>';
}
}
Injection priority:
- After
<head>opening tag (best) - Create
<head>after<html>(fallback) - Wrap entire content in HTML structure (last resort)
Unity WebGL Support
UnityProgress Callback
Purpose: Legacy Unity 5.x games call this to report loading progress
Without polyfill:
UnityProgress(gameInstance, 0.5);
// Error: UnityProgress is not defined
With polyfill:
UnityProgress(gameInstance, 0.5);
// Works: Updates progress, logs completion
Implementation:
window.UnityProgress = function (gameInstance, progress) {
if (!gameInstance.Module) return;
// Initialize progress tracking
if (!gameInstance.progress) {
gameInstance.progress = { loaded: 0, total: 1 };
}
// Update progress
gameInstance.progress.loaded = progress;
// Log completion
if (progress === 1) {
console.log('[Unity] Game loaded successfully');
}
};
createUnityInstance API
Purpose: Unity 2020+ uses this to instantiate games
Without polyfill:
createUnityInstance(canvas, config).then((instance) => {
// Error: createUnityInstance is not defined
});
With polyfill:
createUnityInstance(canvas, config).then((instance) => {
// Works: Returns mock instance
});
Implementation:
window.createUnityInstance = function (canvas, config) {
return new Promise((resolve) => {
console.log('[Unity] createUnityInstance called - using polyfill');
// Return mock Unity instance
resolve({
Module: {}, // Unity WebAssembly module
SetFullscreen: function () {}, // Fullscreen API
SendMessage: function () {}, // Unity-to-JS messaging
Quit: function () {
// Cleanup
return Promise.resolve();
},
});
});
};
UnityLoader2020 Error Handler
Purpose: Suppress Unity framework errors
Without polyfill:
Uncaught ReferenceError: UnityLoader2020 is not defined
With polyfill:
[Unity] Error: ... (logged but not thrown)
Implementation:
if (typeof UnityLoader2020 === 'undefined') {
window.UnityLoader2020 = {
Error: {
handler: function (message, filename, lineno) {
console.warn('[Unity] Error:', message, 'at', filename + ':' + lineno);
return true; // Prevent default error handling
},
},
};
}
General Compatibility
window.external Shim
Purpose: Legacy ActiveX/IE games expect window.external
Without polyfill:
window.external.someMethod();
// Error: Cannot read property 'someMethod' of undefined
With polyfill:
window.external.someMethod();
// Error: someMethod is not a function (better than undefined)
Implementation:
if (typeof window.external === 'undefined') {
window.external = {};
}
AudioContext Polyfill
Purpose: Older browsers use webkitAudioContext
Without polyfill:
const audioContext = new AudioContext();
// Error: AudioContext is not defined (in older Safari/Chrome)
With polyfill:
const audioContext = new AudioContext();
// Works: Uses webkitAudioContext
Implementation:
if (
typeof AudioContext === 'undefined' &&
typeof webkitAudioContext !== 'undefined'
) {
window.AudioContext = webkitAudioContext;
}
Integration with Servers
Proxy Server
private sendResponse(res: ServerResponse, data: Buffer, contentType: string) {
let fileData = data;
// Inject polyfills for HTML files
if (contentType.includes('text/html')) {
fileData = injectPolyfills(data);
logger.info('Injected polyfills into HTML file');
}
res.setHeader('Content-Type', contentType);
res.end(fileData);
}
GameZip Server
// Process HTML files
if (ext === 'html' || ext === 'htm') {
fileData = injectPolyfills(result.data);
logger.info(`Injected polyfills into HTML file: ${relPath}`);
}
Both servers automatically inject polyfills for HTML content
Testing
Test Unity Detection
Create test HTML file:
<!DOCTYPE html>
<html>
<head>
<title>Unity Game</title>
</head>
<body>
<script src="Build/game.loader.js"></script>
<script>
createUnityInstance(canvas, config);
</script>
</body>
</html>
Request file via backend proxy:
curl http://localhost:3100/game-proxy/http://test.com/test-unity.html
Expected output (excerpt):
<!DOCTYPE html>
<html>
<head>
<title>Unity Game</title>
<script>
// Unity WebGL Polyfills
window.UnityProgress = ...
window.createUnityInstance = ...
</script>
<script>
// General game compatibility polyfills
if (typeof window.external === 'undefined') {
window.external = {};
}
</script>
</head>
</html>
Test Non-Unity HTML
Create test HTML file:
<!DOCTYPE html>
<html>
<head>
<title>Regular Game</title>
</head>
<body>
<p>Regular content</p>
</body>
</html>
Request file via backend proxy:
curl http://localhost:3100/game-proxy/http://test.com/test-regular.html
Expected output (excerpt):
<!DOCTYPE html>
<html>
<head>
<title>Regular Game</title>
<script>
// General game compatibility polyfills
// (Unity polyfills NOT injected)
</script>
</head>
</html>
Test Non-HTML File
curl http://localhost:3100/game-proxy/http://test.com/test.swf
Expected: No polyfills injected (binary file returned as-is)
Browser Compatibility
Polyfills tested on:
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
Legacy browser support:
- IE 11: Limited support (some polyfills may not work)
- Chrome < 50: AudioContext polyfill helps
- Safari < 10: WebKit prefix polyfills help
Performance Impact
HTML parsing: ~1-5ms for typical game HTML Regex matching: <1ms for detection String manipulation: ~1-5ms for injection Total overhead: ~5-10ms per HTML file
Memory impact: Negligible (polyfills add ~2KB to HTML)
Caching: Polyfills are injected on every request (no caching)
Security Considerations
No Unsafe Code
Polyfills only add safe, read-only functions:
- No eval()
- No innerHTML manipulation
- No DOM modifications
- No external script loading
Injection Safety
HTML is treated as string, not parsed as DOM:
- No XSS risk from polyfill injection
- Original content preserved
- Only
<head>tag modified
Console Logging
Polyfills log to console for debugging:
- No sensitive information logged
- Helps developers troubleshoot issues
- Can be disabled in production (future)
Troubleshooting
Polyfills Not Injected
Symptom: Unity game fails, no polyfills in HTML
Debug steps:
- Check if Content-Type is
text/html - Verify HTML has
<head>or<html>tag - Check logs for injection message
- View HTML source to confirm injection
Solution: Ensure Content-Type header is correct
Polyfills Not Working
Symptom: Polyfills injected but game still fails
Debug steps:
- Open browser console
- Check for errors before polyfills load
- Verify polyfill code is executed
- Check Unity version compatibility
Solution: Add custom polyfills or update Unity detection
Wrong Polyfills Injected
Symptom: Unity polyfills injected for non-Unity game
Debug steps:
- Check HTML for Unity indicators
- Verify detection regex patterns
- Add exclusion pattern if needed
Solution: Refine detection patterns
Adding New Polyfills
Step 1: Define Polyfill
const NEW_ENGINE_POLYFILLS = `
<script>
// New engine polyfills
window.NewEngine = window.NewEngine || {
init: function() {
console.log('[NewEngine] Initialized');
}
};
</script>
`;
Step 2: Add Detection
function needsNewEnginePolyfills(html: string): boolean {
const indicators = [/NewEngine/i, /newengine\.js/i];
return indicators.some((p) => p.test(html));
}
Step 3: Inject Conditionally
let polyfillsToInject = '';
if (needsUnityPolyfills(htmlString)) {
polyfillsToInject += UNITY_POLYFILLS;
}
if (needsNewEnginePolyfills(htmlString)) {
polyfillsToInject += NEW_ENGINE_POLYFILLS;
}
polyfillsToInject += GENERAL_POLYFILLS;
Step 4: Test
# Create test HTML with new engine
echo '<html><script src="newengine.js"></script></html>' > test-new.html
# Request and verify
curl http://localhost:22500/test-new.html | grep "NewEngine"
Future Enhancements
- Configurable Polyfills: Enable/disable via environment variables
- Custom Polyfills: Load from external files
- Polyfill Library: Support popular polyfill services
- Minification: Minify injected polyfills to reduce size
- Source Maps: Generate source maps for debugging
- Conditional Loading: Load polyfills only if needed by browser
- Performance Metrics: Track polyfill injection time
- A/B Testing: Test polyfill effectiveness
- Error Tracking: Track errors prevented by polyfills
- Documentation: Generate polyfill documentation automatically
References
- Unity WebGL API: https://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html
- MDN Polyfills: https://developer.mozilla.org/en-US/docs/Glossary/Polyfill
- WebKit AudioContext: https://developer.mozilla.org/en-US/docs/Web/API/AudioContext
- window.external: https://developer.mozilla.org/en-US/docs/Web/API/Window/external