# Ruffle Player Implementation Comprehensive documentation for the Ruffle Flash emulator component. ## Overview Ruffle is a Flash Player emulator written in Rust and compiled to WebAssembly. It allows Flash (.swf) content to run in modern browsers without plugins. **Location:** `frontend/src/components/player/RufflePlayer.tsx` ## Architecture ``` RufflePlayer Component ├── Script Loading (GET /ruffle/ruffle.js) ├── Player Initialization (RufflePlayer.newest()) ├── Configuration (scale, quality, etc.) ├── Cleanup Strategy (destroy on unmount) └── UI States (loading, error, success) ``` `/ruffle` is served from different places depending on how the app is running. In development, Vite serves `frontend/public/ruffle` directly. In a container, Ruffle is installed into `config.ruffleDataPath` — `/app/data/ruffle` by default, overridable with `RUFFLE_DATA_PATH` — on the persisted data volume, and Express serves that directory at `/ruffle` ahead of the copy baked into `frontend/dist`. This is what lets an admin's in-app Ruffle update survive the container being recreated: the persisted install shadows the bundled one instead of being overwritten by it. ## Installation Ruffle files are copied from npm package during installation: ```json // package.json { "scripts": { "postinstall": "node copy-ruffle.js" }, "dependencies": { "ruffle-mirror": "^0.1.0" } } ``` ```javascript // copy-ruffle.js const fs = require('fs-extra'); const path = require('path'); const source = path.join(__dirname, 'node_modules', 'ruffle-mirror'); const dest = path.join(__dirname, 'public', 'ruffle'); fs.copySync(source, dest); console.log('✅ Ruffle files copied to public/ruffle/'); ``` ## Component Props ```typescript export interface RufflePlayerProps { swfUrl: string; // URL to .swf file width?: string | number; // Player width (default: '100%') height?: string | number; // Player height (default: '100%') className?: string; // Additional CSS classes onLoadError?: (error: Error) => void; // Error callback onLoadSuccess?: () => void; // Success callback } ``` ## Ruffle Configuration ```typescript player.config = { autoplay: 'auto', // Auto-start playback backgroundColor: '#000000', // Black background letterbox: 'on', // Add letterboxing for aspect ratio unmuteOverlay: 'visible', // Show unmute button if needed contextMenu: true, // Enable right-click menu showSwfDownload: false, // Hide download option upgradeToHttps: window.location.protocol === 'https:', warnOnUnsupportedContent: true, logLevel: 'error', // Only log errors publicPath: '/ruffle/', // Path to Ruffle files scale: 'showAll', // Show all content, maintain aspect forceScale: true, // Prevent runtime scale changes quality: 'high', // High quality rendering allowScriptAccess: 'sameDomain', salign: '', // Center alignment wmode: 'opaque', // Opaque mode }; ``` ## Scale Modes Ruffle supports multiple scale modes: ### showAll (Default) - Shows entire content - Maintains aspect ratio - Adds letterboxing if needed - **Best for most games** ### noscale - No scaling - 1:1 pixel mapping - May cut off content ### exactfit - Scales to fill container - Ignores aspect ratio - May distort content ### noborder - Scales to fill container - Maintains aspect ratio - Crops content if needed ## Cleanup Strategy The component implements a robust cleanup strategy to prevent memory leaks: ### 1. Mount Flag ```typescript let mounted = true; useEffect(() => { // ... initialization return () => { mounted = false; // ... cleanup }; }, [swfUrl]); ``` ### 2. Initialization Lock ```typescript const isInitializingRef = useRef(false); if (isInitializingRef.current) { console.log('[Ruffle] Already initializing, skipping'); return; } isInitializingRef.current = true; ``` ### 3. Delay Before Initialization ```typescript // Wait for any previous cleanup to complete await new Promise((resolve) => setTimeout(resolve, 100)); if (!mounted) return; ``` ### 4. Cleanup Existing Player ```typescript if (rufflePlayerRef.current) { const oldPlayer = rufflePlayerRef.current; rufflePlayerRef.current = null; if (typeof oldPlayer.destroy === 'function') { oldPlayer.destroy(); } else { oldPlayer.remove(); } // Wait for Ruffle's internal cleanup await new Promise((resolve) => setTimeout(resolve, 200)); } ``` ### 5. Unmount Cleanup ```typescript return () => { mounted = false; isInitializingRef.current = false; const playerToCleanup = player || rufflePlayerRef.current; if (playerToCleanup) { if (typeof playerToCleanup.destroy === 'function') { playerToCleanup.destroy(); } else { playerToCleanup.remove(); } } rufflePlayerRef.current = null; // Clear container if (containerRef.current) { while (containerRef.current.firstChild) { containerRef.current.removeChild(containerRef.current.firstChild); } } }; ``` ## Loading States ### Loading UI ```typescript {isLoading && (

Loading Flash game...

Powered by Ruffle

)} ``` ### Error UI ```typescript {error && (
⚠️

Failed to Load Game

{error}

URL: {swfUrl}

This game may require additional files or may not be compatible with Ruffle yet.

)} ``` ## Usage Example ```typescript import { RufflePlayer } from '@/components/player/RufflePlayer'; function FlashGame() { return ( { console.log('Game loaded successfully'); }} onLoadError={(error) => { console.error('Failed to load game:', error); }} /> ); } ``` ## Common Issues ### Issue: Game doesn't scale properly **Solution:** Use `scale: 'showAll'` and `forceScale: true` ### Issue: Multiple player instances **Solution:** Component includes cleanup delays and initialization locks ### Issue: Memory leaks on navigation **Solution:** Component destroys player and clears container on unmount ### Issue: Black screen on load **Solution:** Check browser console for errors, ensure SWF URL is correct ## Browser Console Logging The component includes extensive logging for debugging: ``` [Ruffle] Waiting for any previous cleanup to complete... [Ruffle] Cleaning up existing player instance [Ruffle] Waiting for cleanup to complete... [Ruffle] Loading SWF: /proxy/games/example.swf [Ruffle] SWF loaded successfully [Ruffle] Cleanup: Component unmounting [Ruffle] Cleanup: Removing player instance ``` ## Performance Considerations - **Initial Load:** WebAssembly compilation on first load - **Memory:** Depends on SWF complexity (typically 50-200MB) - **CPU:** Real-time emulation requires modern CPU - **Compatibility:** Some ActionScript 3 features may not be supported ## Ruffle Limitations Not all Flash content is supported: - Some ActionScript 3 features are incomplete - 3D content may have issues - Video playback may be limited - Some external API calls won't work Check [Ruffle Compatibility](https://ruffle.rs/#compatibility) for details. ## Further Reading - [Ruffle Official Website](https://ruffle.rs/) - [Ruffle GitHub](https://github.com/ruffle-rs/ruffle) - [GamePlayer Component](../components/player-components.md)