FreeJoy turns phones and tablets into game controllers for a Windows PC, so a group can play together without owning a separate physical gamepad for every person. One computer hosts the room; players scan a QR code and their touch controls are translated into virtual Xbox 360 controllers.
Example: during a four-player game night, the host opens FreeJoy on the PC and friends join with their phones. Each person receives a player slot, can reconnect after the phone sleeps, and controls the emulator through the same familiar layout.
| Feature | What it means for players |
|---|---|
| QR-code connection | Joining does not require installing or pairing a separate mobile app. |
| Automatic player slots | Up to four people can join without manually configuring every controller. |
| Virtual Xbox 360 devices | Compatible Windows games and emulators see ordinary controller inputs. |
| Session reconnection | Refreshing the browser or waking a phone does not immediately lose the assigned player. |
| Host controls | The person running the game can see, remove, or reset connected players from one screen. |
Maturity and limits: FreeJoy is a working Windows/Ryujinx prototype. It depends on a local network and Python's
vgamepad; latency has not been independently benchmarked, and broader emulator support is not claimed.
FreeJoy is a full-stack controller solution that turns any mobile device into a networked gamepad for PC emulators. It features a Node.js/Socket.IO backend using vgamepad for virtual Xbox 360 controller emulation and a React frontend with a Pro Controller UI.
-
Zero-Config Connection: Scan a single QR code to connect instantly - player slots are auto-assigned.
-
Pro Controller Layout: All players use the same full-featured Pro Controller UI with dual analog sticks, split Joy-Con style layout (cyan left, red right), LED player indicators, and neon-glowing controls.
-
Virtual Gamepad Emulation: Creates true Xbox 360 virtual controllers via Python's
vgamepadlibrary. -
Player Management:
- Real-time connected players list on host screen
- Custom device nicknames (e.g. "iPhone di Antonio")
- Kick individual players (online or offline)
- Reset room to clear all controller leases
-
Persistent Sessions: Auto-reconnect logic restores player slots if the browser refreshes or device sleeps.
-
Auto-Assignment: Players are automatically assigned slots 1-4 in order of connection.
-
Kick Protection: A kick revokes that controller lease and rotates the QR capability without interrupting other players.
-
Premium UI:
- Animated splash screen during connection
- Visual LED player indicator (P1-P4)
- Neon glow effects on all buttons
- Glossy 3D button styling
- Haptic feedback on button press
-
Smart Layouts:
- Landscape Lock: Enforces landscape orientation for maximum playability.
- Safe Area Handling: Optimized for notched phones and tablets.
- Ryujinx (Nintendo Switch) - Primary target
- Node.js 20.19+ (20.x) or 22.12+ and npm
- Python 3.8+ with
vgamepadlibrary - Windows (required for
vgamepadXbox 360 controller emulation)
-
Clone the repository:
git clone https://github.com/aantenore/FreeJoy.git cd FreeJoy -
Install Node.js dependencies:
# Server cd server npm ci # Client cd ../client npm ci
-
Install Python dependencies:
cd ../server/src/python pip install vgamepad
Option 1: PowerShell Launcher (Recommended)
./Launcher.ps1This script:
- Builds the client (production-optimized Vite bundle)
- Starts the Node.js server
- Generates separate host/controller capabilities and opens the authorized host page
Option 2: Manual Start
# Configure distinct capabilities before starting the server.
export FREEJOY_HOST_TOKEN=<at-least-16-base64url-characters>
export FREEJOY_JOIN_TOKEN=<different-at-least-16-base64url-characters>
cd server
npm run dev
# Open http://localhost:3000/#host=<FREEJOY_HOST_TOKEN>.
# Capability URLs are intentionally omitted from process logs.On PowerShell, set the same values with $env:FREEJOY_HOST_TOKEN = "..." and
$env:FREEJOY_JOIN_TOKEN = "...". The launcher creates these values for you.
- Host Screen: Use the launcher or your configured host capability to open the QR screen
- Mobile Devices: Scan the QR code with your phone/tablet camera
- Auto-Assignment: Players are automatically assigned slots P1-P4
- Start Playing: The virtual Xbox 360 controller is ready in Ryujinx!
Host Features:
- View all connected players with real-time status
- See custom device nicknames
- Kick any player (connected or disconnected)
- Reset entire room to start fresh
Player Features:
- Custom nickname on first connection (saved in browser)
- Automatic reconnection on refresh/sleep
- Clear feedback when kicked
The Pro Controller layout maps to Xbox 360 as follows:
| Switch | Xbox 360 |
|---|---|
| A | B |
| B | A |
| X | Y |
| Y | X |
| L / ZL | LB / LT |
| R / ZR | RB / RT |
| D-Pad | D-Pad |
| Left Stick | Left Stick |
| Right Stick | Right Stick |
| - (Minus) | Back |
| + (Plus) | Start |
- Backend: Node.js + Express + Socket.IO + TypeScript
- Frontend: React (Vite) + Socket.IO Client
- Gamepad: Python
vgamepad(Xbox 360 emulation)
- Connection: Client scans QR โ WebSocket connection established
- Assignment: Server auto-assigns slot (1-4) based on availability
- Input Flow:
- Browser captures touch events
- Socket.IO sends to server
- Node.js forwards to Python via stdin
- Python controls virtual Xbox 360 gamepad
- Reconnection: An opaque server-issued capability in localStorage restores the controller lease
- Single Python Process: One process manages all 4 virtual controllers efficiently
- Ephemeral Room IDs: Short alphanumeric codes for easy sharing
- Separate Capabilities: Host administration and controller joining use independently configurable tokens
- Fail-Safe Lifecycle: Disconnect, kick, reset, timeout, and shutdown neutralize each active virtual controller once
- Stateless Sessions: No database - all state in memory for minimal latency
- Device Nicknames: Stored in browser localStorage for personalization
- Controller Lease: A server-directed heartbeat and retry window preserve safe reconnection without allowing socket takeover
- Kick Semantics: Revokes the player lease and rotates the QR capability while preserving other active players
- Log Hygiene: Host, QR, and reconnect capabilities are never written to process logs
| Environment variable | Default | Purpose |
|---|---|---|
PORT |
3000 |
HTTP and Socket.IO port |
PUBLIC_HOST |
detected LAN address | Address placed in the controller QR URL |
FREEJOY_HOST_TOKEN |
random at startup | Host API and administration capability; minimum 16 base64url characters |
FREEJOY_JOIN_TOKEN |
random at startup | Controller join capability; minimum 16 base64url characters |
FREEJOY_INPUT_EVENTS_PER_SECOND |
120 |
Per-controller input ceiling, from 10 to 1000; the current window survives reconnects, and overflow safely releases without creating a host ban |
FREEJOY_CONTROLLER_LEASE_MS |
30000 |
Live and disconnected controller lease window, from 5 seconds to 5 minutes |
FREEJOY_CLEANUP_INTERVAL_MS |
up to 2000 |
Lease cleanup cadence, from 250 ms up to the configured lease |
Capabilities are carried in URL fragments, so browsers do not send them in the initial HTTP request. Host and join values must differ. Kicking a controller rotates the in-memory join capability and refreshes the host QR code. A restart rotates generated capabilities and invalidates previous controller leases.
Edit server/src/plugins/RyujinxPlugin.ts if Python is not in PATH:
this.pythonProcess = spawn('python', [...], {
// Add custom python path if needed
});- Check Windows Device Manager for "Xbox 360 Controller for Windows"
- Ensure
vgamepadis installed:pip show vgamepad - Restart Ryujinx after connecting controllers
- Check server console for Python errors
- QR Code Not Scanning: Ensure phone and PC are on same network
- "Room Full" Error: Maximum 4 players - use Reset Room button
- Kicked Player Rejoining: Use the refreshed host QR; the previous player and QR capabilities are revoked
- Reconnection Fails: Clear browser data and scan QR again
- Use 5GHz WiFi for best performance
- Close background apps on mobile device
- Reduce distance between device and router
FreeJoy/
โโโ client/ # React frontend (Vite)
โ โโโ src/
โ โ โโโ components/
โ โ โ โโโ ProController.tsx # Main controller UI
โ โ โ โโโ InstallBanner.jsx # PWA install prompt
โ โ โโโ App.jsx # Host + routing logic
โ โ โโโ main.jsx
โ โโโ package.json
โโโ server/ # Node.js backend
โ โโโ src/
โ โ โโโ plugins/
โ โ โ โโโ IPlugin.ts # Plugin interface
โ โ โ โโโ RyujinxPlugin.ts # vgamepad integration
โ โ โโโ python/
โ โ โ โโโ virtual_gamepad.py # Xbox controller emulation
โ โ โโโ server.ts # Express + Socket.IO server
โ โ โโโ authority.ts # Host/controller capabilities
โ โ โโโ protocol.ts # Validated controller protocol
โ โ โโโ roomManager.ts # Player/room state
โ โ โโโ wsHandler.ts # WebSocket events
โ โ โโโ types.ts # TypeScript types
โ โโโ package.json
โโโ docs/ # Screenshots
โโโ Launcher.ps1 # PowerShell launcher script
โโโ README.md
- Create plugin in
server/src/plugins/YourEmulator.ts - Implement
IPlugininterface - Wire it in
server/src/server.ts
Edit client/src/components/ProController.tsx - all buttons use inline styles with CSS-in-JS for neon effects.
Primary colors:
- Left Joy-Con:
#00C3E3(Cyan) - Right Joy-Con:
#FF4D6D(Red) - ABXY: Individual neon colors (Green/Red/Yellow/Cyan)
MIT License - See LICENSE file
- vgamepad - Xbox controller emulation
- Socket.IO - Real-time communication
- React Joystick Component - Analog stick UI
Note: This project requires Windows for vgamepad Xbox 360 controller emulation. Linux/Mac support would require alternative virtual gamepad solutions., from the draggable analog sticks to the seamless mobile experience, every line of code was a collaborative dance between human creativity and AI precision.
"Any sufficiently advanced AI is indistinguishable from a very caffeinated developer at 3 AM."
โ Arthur C. Clarke (probably)
Special thanks to Antigravity for:
- ๐จ Making the Controller aesthetics actually look premium
- ๐ Debugging PowerShell scripts that shall not be named
- ๐ฎ Remembering that D-Pads go on the LEFT side of controllers
- ๐ฑ Teaching me that Wi-Fi interfaces have many names (wlan, wl, wi-fi, wireless...)
- โจ And for never judging my "just one more feature" requests
P.S. - If this README seems suspiciously well-organized, that's because an AI wrote it. If you find bugs, that's all me. ๐
Antonio Antenore
Computer Engineer

