Files

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:

  1. After <head> opening tag (best)
  2. Create <head> after <html> (fallback)
  3. 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:

  1. Check if Content-Type is text/html
  2. Verify HTML has <head> or <html> tag
  3. Check logs for injection message
  4. 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:

  1. Open browser console
  2. Check for errors before polyfills load
  3. Verify polyfill code is executed
  4. 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:

  1. Check HTML for Unity indicators
  2. Verify detection regex patterns
  3. 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

  1. Configurable Polyfills: Enable/disable via environment variables
  2. Custom Polyfills: Load from external files
  3. Polyfill Library: Support popular polyfill services
  4. Minification: Minify injected polyfills to reduce size
  5. Source Maps: Generate source maps for debugging
  6. Conditional Loading: Load polyfills only if needed by browser
  7. Performance Metrics: Track polyfill injection time
  8. A/B Testing: Test polyfill effectiveness
  9. Error Tracking: Track errors prevented by polyfills
  10. Documentation: Generate polyfill documentation automatically

References