Competition management platform for Liftoff FPV Simulator. Remotely control your lobby — change tracks, kick players, run playlists with scheduled rotations, and let pilots vote to skip or extend. Run weekly competitions with automatic scoring, league tables, and a playlist calendar that auto-rotates tracks all week. Includes a BepInEx game plugin, Node.js backend, live spectator view, competition page, and admin dashboard. Turn casual lobbies into league nights.
Liftoff Competition transforms a standard Liftoff multiplayer session into a structured, managed event. It connects a BepInEx game plugin running inside Liftoff to a server backend, which powers three web interfaces: a public live view for spectators, a competition page with season and weekly league tables, and an admin dashboard for organisers.
┌──────────────┐ WebSocket ┌──────────────┐ WebSocket ┌──────────────┐
│ Liftoff Game │◄─────────────►│ Server │◄─────────────►│ Admin Panel │
│ (BepInEx │ /ws/plugin │ (Node.js + │ /ws/admin │ (Browser) │
│ Plugin) │ │ Express) │ │ │
└──────────────┘ │ │ └──────────────┘
│ SQLite DB │
│ │ WebSocket ┌──────────────┐
│ Competition │◄─────────────►│ Live View │
│ Runner │ /ws/live │ (Browser) │
└──────────────┘ │ │
├──────────────┤
│ Competition │
│ Page │
└──────────────┘
- The BepInEx plugin captures Photon multiplayer events (races, laps, players, chat) inside Liftoff and sends them to the server over WebSocket.
- The server ingests events into SQLite, manages state, and broadcasts updates to connected web clients.
- The competition runner manages weekly competition lifecycles — activating weeks, rotating playlists, and auto-recovering after server reboots.
- The admin dashboard lets organisers control the lobby: change tracks, run playlists, send chat, manage players, and configure competitions.
- The live view gives spectators a real-time window into the current race, track, and player activity.
- The competition page shows season and weekly league tables with live-updating standings.
- Commands flow back from the server to the plugin to execute lobby changes (track switches, chat messages, kicks) inside the game.
- Change tracks remotely from the admin dashboard
- Kick players when moderation is needed
- Browse and search the available track catalog
- Full command/response protocol between server and game
- Create named playlists with ordered track lists
- Start, stop, pause, and skip through playlists
- Scheduled track rotation with configurable timing
- Resume mid-playlist after server reboot with correct remaining time
- Ideal for league nights, qualifying sessions, tournaments, and curated race events
- Players type
/nextin game chat to start a skip vote (3-minute timer) - Additional players type
/nextto add their vote - Configurable vote threshold — when enough players vote, the track advances automatically
- Players type
/extendin game chat to start an extend vote (3-minute timer) - Uses the same vote threshold as
/next - When the vote passes, 5 minutes are added to the current track timer before it auto-advances
- When the lobby is full (8 players), idle pilots inactive for 5 minutes receive an in-game warning
- After 1 additional minute without activity, they are automatically kicked to free up a slot
- Activity is tracked via gameplay events: checkpoints, laps, race completions, resets, chat messages, race starts, and race ends — so slow pilots on long tracks won't be falsely kicked
- Players can type
/stayin chat to reset their idle timer (adds 5 more minutes) - JMT_Bot (the host) is always immune and hidden from the admin player list
- Additional players can be whitelisted via admin API, admin dashboard, or
IDLE_KICK_WHITELISTenv var - Only active when a playlist is running — free lobbies are unaffected
/info— shows available player commands/next— vote to skip the current track/extend— vote to extend the current track by 5 minutes/stay— reset your idle timer (prevents auto-kick)
- Create seasons with weekly competition weeks
- Automatic weekly lifecycle:
scheduled → active → finalised - Playlist calendar — assign multiple playlists per week; they run back-to-back and repeat for the entire week
- Playlists auto-start when a week begins, no manual intervention needed
- Reboot resilient — deterministic time-based calculation resumes at the correct playlist and track after a server restart, verifying and correcting the in-game track if needed
- Real-time scoring on every race close, plus batch scoring at week finalisation
- Race position — F1-style points (25/18/15/12/10/8/6/4) based on fastest single lap per race
- Most laps — 1 point per 5 laps (capped at 10), plus 5-point lap leader bonus
- Hot streak — 3 points for setting the fastest lap in a race
- Consistency — 3 points if your lap time standard deviation is below the race median (drop worst 20%)
- Most improved — top 3 pilots by % improvement over pre-week personal bests earn 15/10/5 points; 3 points per new personal best on any track
- Participation — 10/20/30 points for flying on 3/5/7 days; 5 bonus points for 3+ different tracks
- Anti-gaming: minimum 2 laps to qualify, solo pilots get no position points, 2 participants = half points
For the full points breakdown, scoring formulas, consistency calculation method, award categories, and everything shown on the competition page, see Competition.md.
- Dedicated
/competition.htmlpage with season and weekly league tables - Season leaderboard aggregating all weeks
- Weekly standings broken down by category (position, laps, consistency, improved, participation, streak)
- Award highlight cards for top performers in each category
- Pilot drill-down showing race-by-race history and points breakdown
- Live updates via WebSocket — standings refresh automatically after each race
- Small competition summary widget on the homepage with top 3 pilots
- Real-time race visualization in the browser
- Current track and environment display
- Active player roster
- Live lap activity feed
- Connection status indicator
- Designed for embedding in streams, club pages, or community sites
- Browser-based control panel for event organisers
- Multi-user authentication — each admin has their own username and password
- Real-time player monitoring with kick controls, idle time display, and whitelist toggle
- Track catalog browsing, selection, and manual track control
- Playlist creation, management, and automated rotation with configurable intervals
- Automated chat templates triggered by track changes, race starts, and race ends
- Competition management — create seasons, generate weeks, assign playlist calendars, recalculate points
- Competition runner — automatic week lifecycle, playlist rotation, and reboot-resilient state recovery
- Live chat log and manual messaging into the game
- Persistent WebSocket connection for real-time status updates across all sections
For a detailed breakdown of every dashboard section, all API endpoints, WebSocket events, and how the playlist and competition runners work, see admin-dashboard.md.
- View in-game chat live in the admin panel
- Send messages directly into the game from the browser
- Automated message templates triggered by events:
track_change— announce the next trackrace_start— notify players a race has begunrace_end— congratulate the winner
- Template variables:
{env},{track},{race},{mins},{winner},{time} - Schedule warning messages before track rotation
- Automatic race and lap recording to SQLite
- Per-pilot tracking via Steam ID and pilot GUID
- Session history and leaderboard support
- Structured JSONL event logs from the plugin
- Race results feed into competition scoring automatically when a competition week is active
| Component | Technology |
|---|---|
| Game Plugin | C# / .NET 4.7.2 / BepInEx / Photon (PUN3) |
| Server | Node.js / Express / WebSocket (ws) |
| Database | SQLite (WAL mode) |
| Validation | AJV (JSON Schema) |
| Frontend | Vanilla JS, HTML, CSS |
| Infrastructure | Docker, Docker Compose, Nginx, Let's Encrypt |
| Tests | Vitest |
Liftoff/
├── contracts/ # Shared event schemas (JSON Schema)
│ ├── common.json
│ ├── lap_recorded.json
│ ├── player_entered.json
│ ├── player_left.json
│ ├── player_list.json
│ ├── race_end.json
│ ├── race_reset.json
│ └── set_track.json
│
├── Pluggins/
│ └── LiftoffPhotonEventLogger/ # BepInEx game plugin
│ ├── LiftoffPhotonEventLogger.cs
│ ├── Features/
│ │ ├── Chat/ # In-game chat capture
│ │ ├── Competition/ # WebSocket client & config
│ │ ├── Identity/ # Player identity tracking
│ │ ├── Logging/ # File logging & serialization
│ │ └── MultiplayerTrackControl/# Track/race/environment control
│ └── docs/
│
├── Server/
│ ├── src/
│ │ ├── index.js # Entry point
│ │ ├── pluginSocket.js # Plugin WebSocket server
│ │ ├── liveSocket.js # Live & admin WebSocket servers
│ │ ├── broadcast.js # Event broadcast dispatcher
│ │ ├── playlistRunner.js # Playlist scheduling & execution
│ │ ├── competitionRunner.js # Weekly competition lifecycle & playlist calendar
│ │ ├── competitionScoring.js # Points engine (real-time + batch)
│ │ ├── state.js # In-memory state
│ │ ├── auth.js # Password hashing & session store
│ │ ├── idleKick.js # Auto-kick idle pilots
│ │ ├── skipVote.js # Vote-to-skip logic
│ │ ├── eventTypes.js # Event type constants
│ │ ├── contracts.js # Event validation
│ │ ├── routes/
│ │ │ ├── admin.js # Admin API endpoints
│ │ │ └── public.js # Public API endpoints
│ │ ├── cli/
│ │ │ └── createUser.js # CLI admin user creation
│ │ └── db/ # SQLite layer
│ │ ├── connection.js # Schema & connection
│ │ ├── competition.js # Competition queries
│ │ ├── ingest.js # Event ingestion
│ │ └── ...
│ │
│ ├── public/
│ │ ├── index.html # Public live view
│ │ ├── competition.html # Competition league tables
│ │ ├── admin.html # Admin dashboard
│ │ └── js/
│ │ ├── public.js # Live view frontend
│ │ ├── competition.js # Competition page frontend
│ │ ├── admin.js # Admin dashboard frontend
│ │ └── shared.js # Shared helpers
│ │
│ ├── nginx/ # Reverse proxy config
│ ├── docker-compose.yml
│ ├── Dockerfile
│ └── .env.example
│
└── Logs/ # Plugin log output
- Server: Node.js 23+ (or Docker)
- Plugin: .NET Framework 4.7.2 SDK, Liftoff with BepInEx installed
-
Clone the repo and configure environment:
cd Server cp .env.example .env -
Edit
.envwith your own secrets:PORT=3000 PLUGIN_API_KEY=your-plugin-key ADMIN_TOKEN=your-admin-token DB_PATH=./competition.db IDLE_KICK_WHITELIST= # comma-separated nicks immune to idle kick ADMIN_USER= # initial admin username (first run only) ADMIN_PASS= # initial admin password (first run only)
-
Run with Docker (recommended):
docker compose up -d
Or run locally:
npm install npm start
-
Create an admin user (choose one method):
Option A — via environment variables: Set
ADMIN_USERandADMIN_PASSin.envbefore first start. The user is created automatically if the database has no users yet.Option B — via CLI:
cd Server node src/cli/createUser.js <username> <password>
You can create additional users the same way. The
ADMIN_TOKENin.envcontinues to work for API/script access via Bearer header. -
Access the interfaces:
- Live view:
http://localhost:3000/ - Competition:
http://localhost:3000/competition.html - Admin panel:
http://localhost:3000/admin.html
- Live view:
-
Install BepInEx 5 into your Liftoff game directory.
-
Build the plugin:
cd Pluggins/LiftoffPhotonEventLogger dotnet buildBy default, the project looks for Liftoff at the standard Steam install path. Set the
LIFTOFF_DIRenvironment variable to override. -
Copy the built DLL into
BepInEx/plugins/in your Liftoff install folder. -
Configure the plugin's connection settings (server URL and API key) to match your server's
.envvalues. -
Launch Liftoff and join a multiplayer session — the plugin connects to the server automatically.
For production with HTTPS:
- Set up your domain's DNS to point to your server.
- Run
init-certs.shto provision Let's Encrypt certificates. - (Optional) Run
init-htpasswd.shto set up Nginx Basic Auth as an extra layer for the admin page. - Create admin users via CLI or env vars (see step 4 above).
- Start with Docker Compose — Nginx handles SSL termination and proxying.
cd Server
npm test- Race organisers — reduce the friction of running structured events
- League admins — automate track rotation and manage sessions remotely
- Community hosts — give your club nights a professional feel
- Streamers — embed the live view for your audience
- Anyone who wants Liftoff multiplayer to feel like an organised event, not an ad-hoc lobby
Contributions are welcome! Feel free to open an issue or submit a pull request.
This project is licensed under the MIT License.