openvibers/source.openvibe.games Commits

Implement OpenVibe next steps foundation

CO Copilot committed 29 Jun 2026 · 965a573d00 · 46 files, +2550 −1478


Changes

ddiff --git a/.gitignore b/.gitignore
iindex cfea6e2..337c366 100644
---- a/.gitignore
++++ b/.gitignore
@@@ -2,12 +2,24 @@ backend/node_modules/
backend/dist/
backend/.env
++# Local Valve SDK checkout. OpenVibe patch sources are tracked in sdk/openvibe/.
++engine/source-sdk-2013/
++
hammer/vmf/*.log
hammer/vmf/*.prt
hammer/vmf/*.lin
hammer/vmf/*.vmx
hammer/vmf/*.bsp
++game/openvibe.games/ServerConfig.vdf
++game/openvibe.games/console.log
++game/openvibe.games/demoheader.tmp
++game/openvibe.games/download/
++game/openvibe.games/downloadlists/
++game/openvibe.games/logs/
++game/openvibe.games/materials/vgui/
++game/openvibe.games/cfg/settings.scr
++
*.core
core
debug.log
ddiff --git a/DEPLOYMENT_STATUS.md b/DEPLOYMENT_STATUS.md
iindex dbc6b1f..d4ba506 100644
---- a/DEPLOYMENT_STATUS.md
++++ b/DEPLOYMENT_STATUS.md
@@@ -1,138 +1,86 @@
--# OpenVibe: Source — Deployment Status
++# OpenVibe: Source Deployment Status
--**Last Updated:** June 28, 2025
--**Version:** 1.0.0 — Complete MVP
--**Status:** ✅ ALL SYSTEMS OPERATIONAL
++**Last updated:** June 28, 2026
++**Status:** Local MVP foundation operational
--## Summary
++## What Runs Locally
--All core features of OpenVibe: Source are now **complete and tested**:
++- Backend API starts and responds on `http://127.0.0.1:3000`.
++- PostgreSQL dev database starts through the repo tooling.
++- Five SRCDS shards boot:
++ - Hub: `ov_hub` on `27015`
++ - Prop Hunt: `ph_openvibe_dev` on `27016`
++ - Deathrun: `dr_openvibe_dev` on `27017`
++ - Fort Wars: `fw_openvibe_dev` on `27018`
++ - Traitor Town: `tt_openvibe_dev` on `27019`
++- Sidecars register and heartbeat those servers with the backend.
++- Travel requests return live local connect targets for every mode.
++- Maps use `ov_join <mode>` portal commands instead of hardcoded connects.
--✅ Custom C++ ConCommands (travel, auth, Prop Hunt disguise)
--✅ Custom HTML/CSS/JS Electron main menu
--✅ Proton-based Windows game client on Linux
--✅ All 5 game modes (Hub, Prop Hunt, Deathrun, Fort Wars, Traitor Town)
--✅ Backend API with server registry & player authentication
--✅ Inter-server travel system with Steam auth
--✅ VScript game logic for each mode
++## Implemented Pieces
--## Running the System
--
--### One-Command Startup (Recommended)
++| Component | Status | Notes |
++| --- | --- | --- |
++| Backend API | Complete for MVP | Dev auth, Steam ticket auth route, inventory, shop, travel, rewards, leaderboard, asset manifest. |
++| PostgreSQL schema | Complete for MVP | Durable players, currency, inventory, servers, join tokens, match results. |
++| Redis sessions | Implemented | Enabled when `SESSION_REDIS_URL` is set. |
++| C++ client travel | Implemented | `ov_join <mode>` calls `/v1/travel/request` and connects with token. |
++| C++ server validation | Implemented | Validates `ov_join_token` against `/v1/travel/validate`. |
++| VGUI menu | Implemented | `openvibe_menu` / `ov_menu` plus `resource/GameMenu.res`. |
++| Prop Hunt disguise | Implemented | Server-side allowlisted model swap/reset. |
++| Fort Wars placement | Implemented | Server-side allowlisted physics prop spawning. |
++| Steam auth | Implemented route | Requires real `STEAM_WEB_API_KEY` and `STEAM_APP_ID` for production. |
++| CDN manifest | Implemented route | Requires real hosted storage under `openvibe.games` for production content. |
++| Production infra | Scaffolded | Docker Compose, Docker Swarm, Kubernetes, nginx CDN config. |
++
++## Verified
```bash
--# Terminal 1: Start all backend services
cd ~/src/openvibe-source/backend
--npm run dev
--
--# Terminal 2: Start all game servers
--cd ~/src/openvibe-source
--bash tools/run-hub.sh &
--bash tools/run-prophunt.sh &
--bash tools/run-deathrun.sh &
--bash tools/run-fortwars.sh &
--bash tools/run-traitortown.sh &
--
--# Terminal 3: Start sidecars
--bash tools/run-sidecar.sh hub hub 27015 48 &
--bash tools/run-sidecar.sh prophunt prophunt 27016 24 &
--bash tools/run-sidecar.sh deathrun deathrun 27017 24 &
--bash tools/run-sidecar.sh fortwars fortwars 27018 32 &
--bash tools/run-sidecar.sh traitortown traitortown 27019 24 &
--
--# Terminal 4: Launch the game
--cd ~/src/openvibe-source/launcher
--npm start
++npm run build
++npm test
```
--### Quick Verification
++Result: TypeScript build passed, 9 tests passed.
```bash
--# Check API health
--curl http://localhost:3000/health | jq '.service'
--
--# Check servers registered
--curl http://localhost:3000/v1/servers | jq '.servers | length'
--
--# Check running processes
--ps aux | grep -E "srcds_linux|ov-sidecar|npm run dev" | grep -v grep | wc -l
++cd ~/src/openvibe-source
++tools/build-sdk-linux.sh
++OPENVIBE_SRCDS_MAP_DELAY=3 OPENVIBE_SRCDS_SMOKE_TIMEOUT=45 tools/smoke-srcds.sh
```
--## Project Status
++Result: Source SDK Linux64 build passed. SRCDS smoke test passed for all five maps.
--| Component | Status | Notes |
--|-----------|--------|-------|
--| Custom ConCommands | ✅ Complete | openvibe_travel.cpp, openvibe_steam_auth.cpp |
--| HTML/CSS/JS Launcher | ✅ Complete | Electron app with 5 tabs (Portal, Servers, etc.) |
--| Game Modes | ✅ Complete | All 5 modes compiled and running |
--| Backend API | ✅ Complete | Server registry, auth, inventory ready |
--| Steam Auth | ✅ Complete | ISteamUser ticket validation |
--| Prop Hunt Disguise | ✅ Complete | Random model swap on hide phase |
--| Proton Integration | ✅ Complete | GE-Proton10-34 → Wine → Vulkan |
++Full stack was also tested with:
--## Architecture
--
--```
--Electron Launcher (HTML/CSS/JS)
-- ↓ [Join Server]
--Proton Wrapper (run-client-proton.sh)
-- ↓ [Launch hl2.exe]
--Wine + DXVK/Vulkan
-- ↓ [Game connects]
--SRCDS (Hub, Prophunt, Deathrun, etc.)
-- ↑
--Backend API (TypeScript/Fastify)
-- ↑
--PostgreSQL
++```bash
++cd ~/src/openvibe-source
++OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh
++node tools/smoke-api.mjs
++tools/dev-down.sh
```
--## Files to Know
--
--- **Game launcher:** `launcher/` (Electron app)
--- **Game launcher entry:** `bash tools/run-client-proton.sh`
--- **Custom ConCommands:** `engine/source-sdk-2013/src/game/server/hl2mp/openvibe_*.cpp`
--- **Game modes:** `game/openvibe.games/scripts/vscripts/ov_*.nut`
--- **Backend API:** `backend/src/app.ts`
--- **Docker compose:** `infra/docker-compose.yml`
--
--## Deployment
--
--### For Production
++## Runtime Note
--1. Update `launcher/main.js` to point to production API URL
--2. Update `game/openvibe.games/cfg/openvibe_common.cfg` with production server details
--3. Distribute launcher via download or CI/CD
--4. Deploy backend to production server
--5. Run game servers on production hardware
++The installed Source SDK Base 2013 Dedicated Server directory provided by the user:
--### For Linux Distribution
--
--The game requires:
--- Steam with SDK Base 2013 Multiplayer installed
--- GE-Proton10-34
--- Node.js 16+
--- PostgreSQL 12+
--
--All bundled in launcher distribution or installed via package manager.
--
--## Known Limitations
++```text
++/mnt/6tb/ssd_offload/home/workstation/.steam/debian-installation/steamapps/common/Source SDK Base 2013 Dedicated Server
++```
--1. **Windows-only game client** — Runs via Proton; native Linux client would need separate build
--2. **sourcetest client.dll** — Current placeholder; full VGUI features need custom client.dll recompile
--3. **No C++ compiler in environment** — Can't recompile GameDLL; would need Visual Studio 2015+
--4. **Local network only** — Servers run on localhost (127.0.0.1); for internet play, update publicHost in code
++contains Windows server binaries in this environment. OpenVibe local Linux testing uses the TF2 Linux64 SRCDS runtime at:
--## Next Steps (Post-MVP)
++```text
++~/srcds/tf2
++```
--- [ ] VGUI menu integration (requires client.dll recompile)
--- [ ] Payment system (backend routes ready)
--- [ ] Anti-cheat (VAC integration)
--- [ ] Competitive ranking
--- [ ] Workshop cosmetics
--- [ ] Discord bot
++## Production Gaps
-----
++Before public deployment:
--**GitHub:** https://github.com/OpenVibers/OpenVibe.Games
--**Date:** June 28, 2025
--**Status:** Ready for deployment
++- Configure real Steam app identity and `STEAM_WEB_API_KEY`.
++- Host cosmetic assets under the `openvibe.games` CDN path.
++- Set real production secrets for API, Redis, PostgreSQL, and admin routes.
++- Add backups, metrics, health checks, log shipping, and deployment secret management.
++- Expand each game mode from prototype rules into full production gameplay.
++- Add moderation/admin UI and anti-abuse controls.
ddiff --git a/PROJECT_INDEX.md b/PROJECT_INDEX.md
iindex d30d5e1..6169ce2 100644
---- a/PROJECT_INDEX.md
++++ b/PROJECT_INDEX.md
@@@ -1,422 +1,108 @@
--# OpenVibe: Source - Complete Project Index
++# OpenVibe: Source Project Index
--## 📦 What You've Received
++## Identity
--This is a **complete, production-ready multiplayer Source engine game** with 5 game modes, MMO-style hub, cosmetics system, backend API, and full Linux deployment guide.
++- Game title: **OpenVibe: Source**
++- Public domain: **OpenVibe.Games**
++- Source mod folder: `game/openvibe.games`
++- Repository root: `~/src/openvibe-source`
-----
++## Repository Map
--## 📂 Project Structure
--
--```
++```text
openvibe-source/
--│
--├── 📘 Documentation (START HERE)
--│ ├── LINUX_SETUP.md ⭐ (12.2 KB)
--│ │ └── Step-by-step Linux deployment guide
--│ ├── QUICK_REFERENCE.md (9.3 KB)
--│ │ └── Common commands, API endpoints, troubleshooting
--│ ├── README.md
--│ │ └── Project overview and quick start
--│ └── docs/
--│ ├── PHASE_2_SUMMARY.md (11.9 KB)
--│ │ └── Detailed implementation of C++/VGUI/Steam auth
--│ └── architecture.md
--│ └── System design and trust model
--│
--├── 🎮 Game Content
--│ └── game/openvibe.games/
--│ ├── scripts/vscripts/ (50k+ lines)
--│ │ ├── ov_shared.nut (Shared utilities)
--│ │ ├── ov_hub.nut (Hub mode - social lobby)
--│ │ ├── ov_prophunt.nut (Prop Hunt - 10.5k lines)
--│ │ ├── ov_deathrun.nut (Deathrun - 10.3k lines)
--│ │ ├── ov_fortwars.nut (Fort Wars - 9.8k lines)
--│ │ └── ov_traitortown.nut (Traitor Town - 11.1k lines)
--│ ├── cfg/ (Per-map auto-exec)
--│ │ ├── ov_hub.cfg
--│ │ ├── ph_openvibe_dev.cfg
--│ │ ├── dr_openvibe_dev.cfg
--│ │ ├── fw_openvibe_dev.cfg
--│ │ └── tt_openvibe_dev.cfg
--│ ├── maps/ (5 compiled BSP files)
--│ │ ├── ov_hub.bsp (32 players)
--│ │ ├── ph_openvibe_dev.bsp (16 players)
--│ │ ├── dr_openvibe_dev.bsp (16 players)
--│ │ ├── fw_openvibe_dev.bsp (16 players)
--│ │ └── tt_openvibe_dev.bsp (16 players)
--│ ├── resource/
--│ │ └── openvibe_english.txt (4k+ localization strings)
--│ └── particles/
--│ ├── particle_manifest.txt
--│ └── openvibe_trails.pcf
--│
--├── 💻 C++ GameDLL (Source SDK 2013)
--│ └── engine/source-sdk-2013/src/game/
--│ ├── server/hl2mp/
--│ │ ├── openvibe_travel.cpp ⭐ (8.1 KB)
--│ │ │ └── ov_join, ov_travel_complete, ov_prophunt_disguise
--│ │ └── openvibe_steam_auth.cpp ⭐ (8.7 KB)
--│ │ └── Steam auth integration, ticket validation
--│ └── client/hl2mp/
--│ └── vgui_openvibe_menu.cpp ⭐ (9.9 KB)
--│ └── Custom main menu VGUI panel (5 tabs, portal buttons)
--│
--├── 🔧 Backend API (Fastify + TypeScript)
--│ └── backend/
--│ ├── src/
--│ │ ├── app.ts (~220 lines)
--│ │ │ ├── GET /v1/leaderboard (top players)
--│ │ │ ├── GET /v1/players/:steamId
--│ │ │ ├── GET /v1/servers (active servers)
--│ │ │ ├── POST /v1/travel/request (auth travel)
--│ │ │ ├── POST /v1/auth/steam (Steam validation)
--│ │ │ ├── POST /v1/matches/end (match rewards)
--│ │ │ └── POST /v1/admin/shop/items (create items)
--│ │ ├── domain.ts (Type definitions)
--│ │ ├── schemas.ts (Validation schemas)
--│ │ ├── repository-pg.ts (PostgreSQL queries)
--│ │ ├── repository-memory.ts (In-memory fallback)
--│ │ └── app.test.ts (6 passing tests)
--│ ├── Dockerfile (Multi-stage build)
--│ └── package.json (Dependencies)
--│
--├── 🌐 Infrastructure
--│ └── infra/
--│ └── docker-compose.yml
--│ ├── PostgreSQL 16 (persistent volume)
--│ └── Fastify API service
--│
--├── 🛠️ Tools & Scripts
--│ └── tools/
--│ ├── generate-dev-vmfs.mjs (~400 lines)
--│ │ └── Programmatic VMF generation for all 5 maps
--│ ├── compile-dev-maps-wine.sh (Enhanced)
--│ │ └── VBSP → VVIS → VRAD compilation pipeline
--│ ├── ov-sidecar.mjs (11.4k lines)
--│ │ └── SRCDS log → API event bridge (VScript events to backend)
--│ ├── run-sidecar.sh
--│ │ └── Sidecar launcher
--│ ├── dev-up.sh
--│ │ └── Start all 5 servers + sidecar in tmux
--│ └── db-migrate.ts
--│ └── Database schema initialization
--│
--└── 📊 Database
-- └── PostgreSQL 16
-- ├── players (SteamID, profile, stats)
-- ├── matches (match history, rewards)
-- ├── inventory (cosmetics, items)
-- ├── shop_items (cosmetics catalog)
-- └── leaderboard (XP rankings)
++├── README.md Main overview and run instructions
++├── DEPLOYMENT_STATUS.md Current local/production status
++├── QUICK_REFERENCE.md Short command reference
++├── LINUX_SETUP.md Linux-first setup notes
++├── backend/ OpenVibe.Games API
++├── docs/ Architecture and implementation notes
++├── engine/ Ignored local Valve SDK checkout
++├── game/openvibe.games/ Source mod content
++├── hammer/vmf/ Editable Hammer maps
++├── infra/ Compose, Swarm, Kubernetes, CDN config
++├── launcher/ Desktop launcher shell
++├── sdk/openvibe/ Tracked C++ GameDLL patch sources
++└── tools/ Build/run/smoke/map tooling
```
-----
--
--## 🚀 Quick Start (Linux)
--
--### 1. Prerequisites & Setup (20 minutes)
--```bash
--cd ~ && git clone https://github.com/yourusername/openvibe-source.git
--cd openvibe-source
--
--# Follow LINUX_SETUP.md sections:
--# 1. Install system dependencies
--# 2. Install SteamCMD and game files
--# 3. Set up PostgreSQL database
--# 4. Build backend
--```
--
--### 2. Compile Maps (30-60 minutes, one-time)
--```bash
--./tools/compile-dev-maps-wine.sh
--# Generates VMF files, runs VBSP/VRAD for all 5 maps
--```
++## Implemented Systems
++
++| Area | Status | Key files |
++| --- | --- | --- |
++| Backend API | Implemented | `backend/src/app.ts`, `backend/src/repository-pg.ts`, `backend/src/sessions.ts` |
++| Backend tests | Passing | `backend/src/app.test.ts` |
++| Steam auth route | Implemented | `POST /v1/auth/steam`; requires production Steam env vars |
++| Redis sessions | Implemented | `backend/src/sessions.ts` |
++| CDN asset manifest | Implemented | `GET /v1/assets/manifest` |
++| C++ client travel | Implemented | `sdk/openvibe/client/hl2mp/openvibe_client.cpp` |
++| C++ main menu | Implemented | `sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp`, `game/openvibe.games/resource/GameMenu.res` |
++| C++ server validation | Implemented | `sdk/openvibe/server/hl2mp/openvibe_server.cpp` |
++| Prop Hunt disguise | Implemented | `ov_prophunt_disguise`, `ov_prophunt_reset_disguise` |
++| Fort Wars placement | Implemented | `ov_fortwars_spawn` |
++| VScript game modes | Implemented prototypes | `game/openvibe.games/scripts/vscripts/ov_*.nut` |
++| Maps | Compiled | `game/openvibe.games/maps/*.bsp` |
++| Production infra | Scaffolded | `infra/docker-compose.yml`, `infra/production/docker-stack.yml`, `infra/kubernetes/openvibe.yaml` |
++
++## Game Modes
++
++| Mode | Map | Port | Join command |
++| --- | --- | ---: | --- |
++| Hub | `ov_hub` | 27015 | `ov_join hub` |
++| Prop Hunt | `ph_openvibe_dev` | 27016 | `ov_join prophunt` |
++| Deathrun | `dr_openvibe_dev` | 27017 | `ov_join deathrun` |
++| Fort Wars | `fw_openvibe_dev` | 27018 | `ov_join fortwars` |
++| Traitor Town | `tt_openvibe_dev` | 27019 | `ov_join traitortown` |
++
++## Build And Test
++
++Backend:
--### 3. Start All Servers (2 minutes)
```bash
--./tools/dev-up.sh
--# Launches 5 game servers + API + sidecar in tmux
--```
--
--### 4. Connect & Play (1 minute)
--```
--In Half-Life 2 Deathmatch console:
--> connect 127.0.0.1:27015
--> ov_join hub
--```
--
--See **QUICK_REFERENCE.md** for detailed commands and troubleshooting.
--
-----
--
--## 📋 Features by Component
--
--### Game Modes (VScript - 50k+ LOC)
--- ✅ **Hub** - Social lobby with NPCs, shops, inventory syncing
--- ✅ **Prop Hunt** - Props disguise vs hunters hunting them
--- ✅ **Deathrun** - Runners escape activators who control traps
--- ✅ **Fort Wars** - Build phase → combat phase with structures
--- ✅ **Traitor Town** - Social deduction: traitors vs innocents + detective
--
--### Backend API (TypeScript - 15k+ LOC)
--- ✅ Player profiles & progression (XP, currency, levels)
--- ✅ Leaderboard system (rank by XP)
--- ✅ Match history & reward calculation
--- ✅ Cosmetics shop (buy player models, trails, skins)
--- ✅ Inventory management (items synced across servers)
--- ✅ Authenticated server travel (ov_join command)
--- ✅ Steam authentication integration
--
--### Client Features (C++ VGUI)
--- ✅ Custom main menu replacing server browser
--- ✅ Portal buttons for all 5 game modes
--- ✅ Quick-join functionality with token authentication
--- ✅ Tab interface (Portal, Shop, Leaderboard, Inventory)
--
--### Game Mechanics (C++)
--- ✅ `ov_join <mode>` ConCommand for server travel
--- ✅ Authenticated token validation (30s expiration)
--- ✅ `ov_prophunt_disguise <model>` for prop swapping
--- ✅ 10 physics prop models supported
--- ✅ Steam ticket validation with SteamGameServer API
--- ✅ VAC ban detection & session hijack prevention
--
--### Infrastructure (Docker)
--- ✅ PostgreSQL 16 with persistent volume
--- ✅ Fastify API service with health checks
--- ✅ Sidecar process (Node.js) bridging SRCDS logs to API
--- ✅ Automated database migrations
--- ✅ Multi-server tmux management
--
-----
--
--## 📚 Documentation
--
--| Document | Purpose | Read Time |
--|----------|---------|-----------|
--| **LINUX_SETUP.md** | Complete Linux deployment guide | 20 min |
--| **QUICK_REFERENCE.md** | Commands, API, troubleshooting | 5 min |
--| **PHASE_2_SUMMARY.md** | C++ implementation details | 10 min |
--| **docs/architecture.md** | System design & data flow | 15 min |
--| **README.md** | Project overview | 5 min |
--
--**Start with:** `LINUX_SETUP.md` → Follow step-by-step → `QUICK_REFERENCE.md` for commands
--
-----
--
--## 🎯 What's Complete (Phase 2)
--
--- ✅ **Custom C++ GameDLL** (openvibe_travel.cpp, 8.1 KB)
-- - ov_join ConCommand for authenticated travel
-- - Travel request HTTP callbacks
-- - Server-to-server token handoff
--
--- ✅ **Custom VGUI Main Menu** (vgui_openvibe_menu.cpp, 9.9 KB)
-- - 5 portal buttons (Hub, Prop Hunt, Deathrun, Fort Wars, Traitor Town)
-- - Tab-based UI (Portal, Shop, Leaderboard, Inventory)
-- - Quick-join functionality
--
--- ✅ **Prop Hunt Disguise** (in C++ & VScript)
-- - ov_prophunt_disguise ConCommand
-- - 10 validated physics prop models
-- - Model swapping + weapon removal + name hiding
--
--- ✅ **Steam Authentication** (openvibe_steam_auth.cpp, 8.7 KB)
-- - ISteamGameServer::BeginAuthSession integration
-- - Ticket validation & expiration
-- - VAC ban detection
-- - Session hijack prevention
--
--- ✅ **BSP Compilation Pipeline** (compile-dev-maps-wine.sh)
-- - VMF generation automation
-- - VBSP (geometry), VVIS (visibility), VRAD (lighting)
-- - All 5 maps compiled ready for deployment
--
--- ✅ **Linux Setup & Deployment Guide** (LINUX_SETUP.md, 12.2 KB)
-- - Prerequisites (Ubuntu 20.04+)
-- - SteamCMD + game installation
-- - PostgreSQL database setup
-- - Backend configuration
-- - Multiple server launch methods
-- - Troubleshooting section
-- - Production deployment guide
--
-----
--
--## 🔗 Integration Points
--
--```
--Client Server
--├─ ov_join hub ──────────────→ HTTP POST /v1/travel/request
--│ │
--│ └─ Validates, returns {connect, token}
--│
--├─ connect IP + ov_auth ──────→ ov_travel_complete token
--│ │
--│ └─ Validates token, spawns player
--│
--└─ Authenticated gameplay ────→ VScript game logic runs
-- │
-- └─ Emits [OV] events to sidecar
-- │
-- └─ Sidecar sends to API
-- │
-- └─ Backend records match + rewards
--```
--
-----
--
--## 🛠️ Build Instructions
--
--### Compile C++ GameDLL
--```bash
--cd engine/source-sdk-2013
--./createsln.bat # On Windows (or use Linux Visual Studio Code)
--
--# In Visual Studio:
--# 1. Add openvibe_*.cpp files to hl2mp_dll project
--# 2. Build Solution (Release)
--# 3. Output: mod_hl2mp.dll
--# 4. Copy to: $SRCDS_DIR/hlserver/orangebox/hl2mp/bin/
--```
--
--### Compile Backend
--```bash
--cd backend
--npm install
++cd ~/src/openvibe-source/backend
npm run build
--npm test # Should pass 6 tests
--docker build -t openvibe_api .
++npm test
```
--### Compile Maps
++Source SDK:
++
```bash
--./tools/compile-dev-maps-wine.sh
--# Generates: ov_hub.bsp, ph_openvibe_dev.bsp, etc.
++cd ~/src/openvibe-source
++tools/build-sdk-linux.sh
++tools/setup-openvibe-bin.sh
```
-----
--
--## 📊 Project Stats
--
--| Metric | Count |
--|--------|-------|
--| Total LOC | 100k+ |
--| VScript | 50k+ lines |
--| C++ | 26 KB |
--| TypeScript | 15k+ lines |
--| Node.js | 11.4k lines (sidecar) |
--| Documentation | 50k+ words |
--| Game Modes | 5 |
--| Maps | 5 |
--| Concurrent Players | 160 (32 × 5 servers) |
--| Database Tables | 6 |
--| API Endpoints | 20+ |
--| Supported Platforms | Linux (Ubuntu 20.04+) |
--
-----
--
--## 🎮 How to Play
--
--### Starting a Match
--1. Connect to Hub server (port 27015)
--2. Open main menu (F6 or custom key)
--3. Click game mode button (e.g., "Prop Hunt")
--4. Authenticate with Steam
--5. Spawn in selected game mode
--
--### Game Mode Examples
--
--**Prop Hunt:**
--- Hunters have 30 seconds before props start moving
--- Props disguise as physics objects: `ov_prophunt_disguise models/props_c17/can_stack_01a.mdl`
--- Props must evade hunters for 8 minutes to win
--- Hunters must eliminate all props
--
--**Deathrun:**
--- Runners must reach the end of a trap-filled map
--- Activators control traps: buttons, doors, hazards
--- Round ends when runners escape or activators eliminate all runners
--
--**Traitor Town:**
--- 3-4 traitors among innocents
--- Detective investigates deaths
--- Innocents vote to eliminate suspects
--- Traitors secretly eliminate innocents
++SRCDS:
-----
--
--## 🐛 Troubleshooting
--
--### "Map won't load"
```bash
--# Check BSP exists
--ls -la game/openvibe.games/maps/*.bsp
--
--# Copy to SRCDS
--cp game/openvibe.games/maps/*.bsp $SRCDS_DIR/hlserver/orangebox/hl2mp/maps/
++cd ~/src/openvibe-source
++OPENVIBE_SRCDS_MAP_DELAY=3 OPENVIBE_SRCDS_SMOKE_TIMEOUT=45 tools/smoke-srcds.sh
```
--### "API connection failed"
--```bash
--# Check backend
--curl http://localhost:3000/health
++Full stack:
--# Check database
--psql openvibe -c "SELECT 1;"
--```
--
--### "VScript errors in console"
```bash
--# Enable debug
--sv_script_debug 1
--
--# Check script files
--ls -la game/openvibe.games/scripts/vscripts/
--
--# View logs
--tail -f $SRCDS_DIR/hlserver/orangebox/hl2mp/logs/L*.log
++cd ~/src/openvibe-source
++OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh
++node tools/smoke-api.mjs
++tools/dev-down.sh
```
--See **QUICK_REFERENCE.md** for more troubleshooting tips.
--
-----
--
--## 📞 Support
--
--- **Setup Issues**: See `LINUX_SETUP.md`
--- **Command Reference**: See `QUICK_REFERENCE.md`
--- **API Docs**: See backend/README.md
--- **Architecture**: See docs/architecture.md
--- **Implementation**: See docs/PHASE_2_SUMMARY.md
--
-----
--
--## 🎉 Next Steps
--
--1. **Follow the setup guide** - `LINUX_SETUP.md` (step-by-step)
--2. **Test locally** - Start dev servers and connect
--3. **Customize** - Edit VScript files for your game rules
--4. **Deploy** - Use production configs from LINUX_SETUP.md
--5. **Monetize** - Add payment processor for cosmetics shop
--6. **Market** - Launch on Steam with your game
--
-----
--
--## ✅ Project Status
--
--**🎯 COMPLETE & READY FOR DEPLOYMENT**
++## Verified On June 28, 2026
--All requested features implemented:
--- ✅ Custom C++ GameDLL (ov_join, prop disguise)
--- ✅ Custom VGUI main menu
--- ✅ Steam authentication
--- ✅ BSP compilation pipeline
--- ✅ Linux setup guide (comprehensive)
++- Backend TypeScript build passed.
++- Backend Vitest suite passed: 9 tests.
++- Source SDK 2013 Linux64 build passed with OpenVibe C++ patch applied.
++- SRCDS smoke test passed for all five maps.
++- Full dev stack registered all five servers and returned valid travel targets for every mode.
--**Next:** Follow `LINUX_SETUP.md` and launch your OpenVibe servers!
++## Production Readiness
-----
++The local MVP foundation is operational. Public deployment still needs:
--**Built with ❤️ using Source SDK 2013, Fastify, PostgreSQL, Docker, and TypeScript**
++- real Steam app credentials
++- hosted CDN storage for player models/trails
++- production PostgreSQL/Redis with backups
++- deployment secrets management
++- metrics/logging/alerts
++- expanded game mode content and moderation tools
ddiff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md
iindex 7868455..4863c21 100644
---- a/QUICK_REFERENCE.md
++++ b/QUICK_REFERENCE.md
@@@ -1,411 +1,139 @@
--# OpenVibe: Source - Quick Reference
++# OpenVibe: Source Quick Reference
--## File Locations
++## Key Paths
--### C++ GameDLL Components
--```
--engine/source-sdk-2013/src/game/
--├── server/hl2mp/
--│ ├── openvibe_travel.cpp # ov_join, prop disguise
--│ └── openvibe_steam_auth.cpp # Steam auth integration
--└── client/hl2mp/
-- └── vgui_openvibe_menu.cpp # Custom main menu UI
--```
--
--### Game Content
--```
--game/openvibe.games/
--├── scripts/vscripts/
--│ ├── ov_shared.nut # Shared utilities
--│ ├── ov_hub.nut # Hub mode
--│ ├── ov_prophunt.nut # Prop Hunt
--│ ├── ov_deathrun.nut # Deathrun
--│ ├── ov_fortwars.nut # Fort Wars
--│ └── ov_traitortown.nut # Traitor Town
--├── cfg/
--│ ├── ov_hub.cfg # Auto-exec
--│ ├── ph_openvibe_dev.cfg
--│ ├── dr_openvibe_dev.cfg
--│ ├── fw_openvibe_dev.cfg
--│ └── tt_openvibe_dev.cfg
--├── maps/
--│ ├── ov_hub.vmf / .bsp
--│ ├── ph_openvibe_dev.vmf / .bsp
--│ ├── dr_openvibe_dev.vmf / .bsp
--│ ├── fw_openvibe_dev.vmf / .bsp
--│ └── tt_openvibe_dev.vmf / .bsp
--├── resource/
--│ └── openvibe_english.txt
--└── particles/
-- ├── particle_manifest.txt
-- └── openvibe_trails.pcf
++```text
++game/openvibe.games/ Source mod folder
++sdk/openvibe/ Tracked OpenVibe C++ patch sources
++engine/source-sdk-2013/ Local ignored Valve SDK checkout
++backend/ Fastify API
++hammer/vmf/ Editable VMF maps
++game/openvibe.games/maps/ Compiled BSP maps
++infra/ Local and production infra templates
++tools/ Build, run, smoke, and map tools
```
--### Backend
--```
--backend/
--├── src/
--│ ├── app.ts # Fastify app + endpoints
--│ ├── domain.ts # Type definitions
--│ ├── schemas.ts # Validation schemas
--│ ├── repository-pg.ts # PostgreSQL queries
--│ └── app.test.ts # Tests
--└── Dockerfile
--```
++## Build
--### Infrastructure
--```
--infra/
--└── docker-compose.yml # PostgreSQL + API
--tools/
--├── generate-dev-vmfs.mjs # VMF generation
--├── compile-dev-maps-wine.sh # BSP compilation
--├── ov-sidecar.mjs # Log bridge to API
--├── run-sidecar.sh # Sidecar launcher
--└── dev-up.sh # Start all servers
--```
--
--### Documentation
--```
--LINUX_SETUP.md # Linux deployment guide
--docs/
--├── PHASE_2_SUMMARY.md # Implementation summary
--├── architecture.md # System design
--└── README.md # Main overview
--```
--
-----
--
--## Quick Start (Linux)
--
--### 1. Initial Setup (15-20 minutes)
--```bash
--# Set up Steam/SteamCMD
--mkdir -p ~/steamcmd && cd ~/steamcmd
--curl -sqL "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" | tar zxvf -
--
--# Install game files
--./steamcmd.sh +login anonymous +app_update 320 validate +quit # HL2:DM
--./steamcmd.sh +login anonymous +app_update 440 validate +quit # TF2
--
--# Install dependencies
--sudo apt-get install -y wine wine32 postgresql postgresql-contrib docker.io node.js npm
--
--# Clone project
--cd ~
--git clone https://github.com/yourusername/openvibe-source.git
--cd openvibe-source
--```
--
--### 2. Database Setup (5 minutes)
```bash
--# Start PostgreSQL
--sudo systemctl start postgresql
--
--# Initialize database
--cd backend && npm install && npm run db:migrate
--
--# Verify
++cd ~/src/openvibe-source/backend
++npm run build
npm test
```
--### 3. Map Compilation (30-60 minutes, one-time)
```bash
--# Compile BSPs
--VVIS_ENABLED=0 ./tools/compile-dev-maps-wine.sh
--
--# Verify output
--ls -lh game/openvibe.games/maps/*.bsp
++cd ~/src/openvibe-source
++tools/build-sdk-linux.sh
++tools/setup-openvibe-bin.sh
```
--### 4. Start Servers (2 minutes)
--```bash
--# Terminal 1: Start API
--cd backend && npm start
--
--# Terminal 2: Start game servers
--./tools/dev-up.sh
--
--# Attach to monitor
--tmux attach-session -t openvibe
--```
--
--### 5. Connect Client (1 minute)
--```bash
--# In Half-Life 2 Deathmatch console:
--> connect 127.0.0.1:27015
--> ov_join hub
--```
--
-----
--
--## ConCommands
++## Run Locally
--### Client (in-game console)
```bash
--# Travel to game mode
--ov_join hub # Hub server
--ov_join prophunt # Prop Hunt
--ov_join deathrun # Deathrun
--ov_join fortwars # Fort Wars
--ov_join traitortown # Traitor Town
--
--# Authentication
--ov_auth_ticket <hex> # Send auth ticket to server
--ov_auth_confirm <token> # Confirm authentication
--
--# Server Connection
--connect 127.0.0.1:27015 # Connect to Hub (port 27015)
--disconnect # Leave server
++cd ~/src/openvibe-source
++OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh
```
--### Server (SRCDS console)
--```bash
--# Prop Hunt
--ov_prophunt_disguise models/props_c17/can_stack_01a.mdl
--
--# Debugging
--developer 2
--sv_script_debug 1
--
--# Map changes
--changelevel ph_openvibe_dev
--```
--
-----
--
--## API Endpoints
--
--### Public
--```bash
--# Get leaderboard (top 10 players)
--curl http://localhost:3000/v1/leaderboard?limit=10
--
--# Get player profile
--curl http://localhost:3000/v1/players/76561198123456789
--
--# Get active servers
--curl http://localhost:3000/v1/servers
++Stop:
--# Initiate travel
--curl -X POST http://localhost:3000/v1/travel/request \
-- -H "Content-Type: application/json" \
-- -d '{"steamId":"76561198...", "targetMode":"prophunt"}'
--```
--
--### Admin (requires X-Admin-Secret header)
```bash
--# Create shop item
--curl -X POST http://localhost:3000/v1/admin/shop/items \
-- -H "X-Admin-Secret: your-secret" \
-- -H "Content-Type: application/json" \
-- -d '{"name":"Cool Skin","type":"model","price":5000}'
--
--# Give player currency
--curl -X POST http://localhost:3000/v1/admin/players/76561198.../balance \
-- -H "X-Admin-Secret: your-secret" \
-- -H "Content-Type: application/json" \
-- -d '{"amount":10000}'
++cd ~/src/openvibe-source
++tools/dev-down.sh
```
-----
--
--## Game Mode Servers
--
--| Mode | Port | Max Players | Map |
--|------|------|-------------|-----|
--| Hub | 27015 | 32 | ov_hub |
--| Prop Hunt | 27016 | 16 | ph_openvibe_dev |
--| Deathrun | 27017 | 16 | dr_openvibe_dev |
--| Fort Wars | 27018 | 16 | fw_openvibe_dev |
--| Traitor Town | 27019 | 16 | tt_openvibe_dev |
--
-----
--
--## Troubleshooting
++## Smoke Tests
--### "Map failed to load"
```bash
--# Check BSP exists
--ls -la game/openvibe.games/maps/*.bsp
--
--# Verify it copied to SRCDS
--ls -la $SRCDS_DIR/hlserver/orangebox/hl2mp/maps/
++cd ~/src/openvibe-source
++node tools/smoke-api.mjs
++OPENVIBE_SRCDS_MAP_DELAY=3 OPENVIBE_SRCDS_SMOKE_TIMEOUT=45 tools/smoke-srcds.sh
```
--### "API not responding"
--```bash
--# Check backend is running
--curl http://localhost:3000/health
++Expected SRCDS output:
--# Check database connection
--cd backend && npm run db:check
++```text
++[srcds:hub] ok (ov_hub)
++[srcds:prophunt] ok (ph_openvibe_dev)
++[srcds:deathrun] ok (dr_openvibe_dev)
++[srcds:fortwars] ok (fw_openvibe_dev)
++[srcds:traitortown] ok (tt_openvibe_dev)
```
--### "VScript errors"
--```bash
--# Enable debug mode
--sv_script_debug 1
++## Maps
--# Check script files
--ls -la game/openvibe.games/scripts/vscripts/
--
--# Check logs
--tail -f $SRCDS_DIR/hlserver/orangebox/hl2mp/logs/L*.log
--```
--
--### "Can't connect to server"
```bash
--# Check firewall
--sudo ufw allow 27015:27020/udp
--sudo ufw allow 27015:27020/tcp
--
--# Verify SRCDS listening
--netstat -tulnp | grep -E "27015|27016"
--
--# Check firewall status
--sudo ufw status
++cd ~/src/openvibe-source
++node tools/generate-dev-vmfs.mjs
++tools/compile-dev-maps-wine.sh
```
-----
++Portal pads currently emit:
--## Environment Variables
--
--```bash
--# Set in ~/.bashrc
--export OPENVIBE_ROOT="$HOME/openvibe-source"
--export TF2_INSTALL_DIR="$HOME/.steam/steam/SteamApps/common/Team Fortress 2"
--export HL2_INSTALL_DIR="$HOME/.steam/steam/SteamApps/common/Half-Life 2 Deathmatch"
--export SRCDS_DIR="$HOME/srcds"
--export OV_API_ENDPOINT="http://localhost:3000"
--export OV_API_SECRET="your-secret-key"
--export DATABASE_URL="postgresql://openvibe:password@localhost:5432/openvibe"
++```text
++ov_join hub
++ov_join prophunt
++ov_join deathrun
++ov_join fortwars
++ov_join traitortown
```
-----
++## Client / Server Commands
--## Performance Tuning
++| Command | Side | Purpose |
++| --- | --- | --- |
++| `ov_join <mode>` | Client | Request travel token and connect to server. |
++| `ov_auth_steam` | Client | Send Steam Web API ticket to backend auth. |
++| `openvibe_menu` / `ov_menu` | Client | Open OpenVibe VGUI menu. |
++| `ov_open_url <url>` | Client | Open OpenVibe web page in Steam/browser. |
++| `ov_prophunt_disguise <model>` | Server | Apply allowlisted prop disguise. |
++| `ov_prophunt_reset_disguise` | Server | Restore player model. |
++| `ov_fortwars_spawn <type>` | Server | Spawn allowlisted build prop. |
--### SRCDS
--```bash
--# Rate limits (in server.cfg)
--rate 30000
--cl_cmdrate 66
--sv_mincmdrate 10
--
--# Tickrate (higher = more CPU, more responsive)
--sv_tickrate 66
--```
++## API
--### Database
```bash
--# Connect to PostgreSQL and check
--sudo -u postgres psql openvibe
--> SELECT COUNT(*) FROM players;
--> SELECT * FROM matches ORDER BY created_at DESC LIMIT 1;
--> ANALYZE; # Update statistics
++curl -s http://127.0.0.1:3000/health | jq
++curl -s http://127.0.0.1:3000/v1/servers | jq
++curl -s http://127.0.0.1:3000/v1/assets/manifest | jq
```
-----
++Travel request:
--## Monitoring
--
--### Check All Services
```bash
--# SRCDS processes
--ps aux | grep srcds | grep -v grep
--
--# API status
--curl http://localhost:3000/health
--
--# Database status
--sudo systemctl status postgresql
--
--# Docker status
--docker ps | grep openvibe
++curl -s http://127.0.0.1:3000/v1/travel/request \
++ -H 'Content-Type: application/json' \
++ -d '{"steamId":"76561198000000000","mode":"prophunt"}' | jq
```
--### View Logs
--```bash
--# SRCDS
--tail -f $SRCDS_DIR/hlserver/orangebox/hl2mp/logs/L*.log
--
--# API
--docker logs -f openvibe_api
++## SRCDS Runtime
--# Sidecar (in tmux)
--tmux capture-pane -t openvibe:sidecar -p
++The working local Linux64 runtime is:
--# PostgreSQL
--sudo tail -f /var/log/postgresql/postgresql.log
++```text
++~/srcds/tf2
```
-----
--
--## Useful Scripts
++The installed Source SDK Base 2013 Dedicated Server directory in `/mnt/6tb/...` contains Windows server binaries in this environment, so it is not used for the Linux64 smoke tests.
--### Restart All Services
--```bash
--#!/bin/bash
--# Kill old processes
--killall srcds_i486 2>/dev/null || true
--tmux kill-session -t openvibe 2>/dev/null || true
++## Production Values
--# Restart backend
--docker-compose -f infra/docker-compose.yml restart
++Set these before real deployment:
--# Start servers
--./tools/dev-up.sh
--
--echo "All services restarted"
++```text
++DATABASE_URL
++SESSION_REDIS_URL
++STEAM_WEB_API_KEY
++STEAM_APP_ID
++OPENVIBE_ADMIN_SECRET
++OPENVIBE_CDN_BASE_URL
```
--### Backup Database
--```bash
--#!/bin/bash
--sudo -u postgres pg_dump openvibe > openvibe_backup_$(date +%Y%m%d_%H%M%S).sql
--```
--
--### Check Server Health
--```bash
--#!/bin/bash
--for port in 27015 27016 27017 27018 27019; do
-- if netstat -tulnp 2>/dev/null | grep -q ":$port"; then
-- echo "✓ Port $port (running)"
-- else
-- echo "✗ Port $port (down)"
-- fi
--done
--```
--
-----
--
--## Common Issues & Solutions
--
--| Issue | Solution |
--|-------|----------|
--| "SRCDS not found" | `export SRCDS_DIR=~/srcds` and run setup |
--| "TF2 tools not found" | Install TF2 via SteamCMD, set `TF2_INSTALL_DIR` |
--| "Wine not found" | `sudo apt-get install wine wine32` |
--| "Database connection failed" | Start PostgreSQL: `sudo systemctl start postgresql` |
--| "Port already in use" | Kill existing SRCDS: `pkill -f srcds` |
--| "VScript not loading" | Verify cfg files in `game/openvibe.games/cfg/` |
--| "API timeout" | Check backend logs: `docker logs openvibe_api` |
--
-----
--
--## Next Steps
--
--1. **Customize** - Edit VScript files for your own game rules
--2. **Extend** - Add more game modes or cosmetics
--3. **Deploy** - Follow production guide for public servers
--4. **Monetize** - Integrate payment processor for shop
--5. **Market** - Launch on Steam with your game
--
-----
--
--**For detailed setup instructions, see:** [`LINUX_SETUP.md`](./LINUX_SETUP.md)
--
--**For architecture details, see:** [`docs/architecture.md`](./docs/architecture.md)
++## Common Fixes
--**For implementation details, see:** [`docs/PHASE_2_SUMMARY.md`](./docs/PHASE_2_SUMMARY.md)
++| Issue | Fix |
++| --- | --- |
++| API port still busy | `tools/dev-down.sh`, then check for leftover OpenVibe processes. |
++| Server says map missing | Re-run `tools/compile-dev-maps-wine.sh`. |
++| Game DLLs stale | Run `tools/build-sdk-linux.sh` then `tools/setup-openvibe-bin.sh`. |
++| Hammer++ launch fails in Lutris | Use Steam non-game entry with Proton Experimental. |
++| Steam auth returns 501 | Set `STEAM_WEB_API_KEY` and `STEAM_APP_ID`. |
ddiff --git a/README.md b/README.md
iindex 96e4677..b947822 100644
---- a/README.md
++++ b/README.md
@@@ -1,10 +1,11 @@
# OpenVibe: Source
--Linux-first Source SDK 2013 multiplayer project for `OpenVibe: Source`.
++Linux-first Source SDK 2013 multiplayer project for **OpenVibe: Source**.
- Public identity: `OpenVibe.Games`
- Source mod folder: `game/openvibe.games`
- Backend/API: `backend/`
++- Tracked OpenVibe C++ patch sources: `sdk/openvibe/`
- VScript game logic: `game/openvibe.games/scripts/vscripts/`
- Editable maps: `hammer/vmf/`
- Compiled maps: `game/openvibe.games/maps/`
@@@ -12,170 +13,168 @@ Linux-first Source SDK 2013 multiplayer project for `OpenVibe: Source`.
## Current State
--### ✅ Implemented
--
--- **Backend API** — Fastify/TypeScript with dev auth, player profiles, shop, inventory,
-- server registry, travel tokens, match rewards, leaderboard, and admin shop management.
--- **PostgreSQL migration** — players, inventory, shop, servers, join tokens, currency
-- ledger, and match results.
--- **In-memory repository** — fast local tests without a database.
--- **6/6 backend tests** — vertical slices covering all major flows.
--- **VScript game logic** — Squirrel scripts for all five servers:
-- - `ov_shared.nut` — shared utilities, event emission protocol.
-- - `ov_hub.nut` — hub heartbeat, portal pad labels, player join.
-- - `ov_prophunt.nut` — role assignment, hide phase, hunt phase, rewards.
-- - `ov_deathrun.nut` — activator vs. runners, trap system, finish line.
-- - `ov_fortwars.nut` — two-team build phase → combat phase, kill tracking.
-- - `ov_traitortown.nut` — role assignment (traitors / detective / innocents), win conditions.
--- **Per-map auto-exec CFGs** — `cfg/<mapname>.cfg` automatically loads the VScript when the map starts.
--- **Sidecar process** (`ov-sidecar.mjs`) — tails SRCDS log files, bridges `[OV]` events to the backend API (heartbeat, match rewards).
--- **VMF generator updated** — `logic_script`, `game_text` HUD entities, team spawn points now in all generated maps.
--- **Five compiled BSP maps** — hub + Prop Hunt, Deathrun, Fort Wars, Traitor Town.
--- **Docker Compose** (`infra/docker-compose.yml`) — PostgreSQL + API service.
--- **Localization** (`resource/openvibe_english.txt`) — English strings for all modes.
--- **Particle manifest** — placeholder for trail particles.
--- **Dev tools** — compile, run, register, and full dev orchestration scripts.
--
--## Custom ConCommands (C++)
--
--All game logic is driven by authenticated ConCommands:
--
--| Command | Location | Purpose |
--|---------|----------|---------|
--| `ov_join <mode>` | `openvibe_travel.cpp` | Travel to hub, prophunt, deathrun, etc. |
--| `ov_auth_ticket <ticket>` | `openvibe_steam_auth.cpp` | Authenticate player with Steam ticket |
--| `ov_auth_confirm <token>` | `openvibe_steam_auth.cpp` | Confirm auth with backend token |
--| `ov_prophunt_disguise <model>` | `openvibe_travel.cpp` | Swap player model to random prop |
--| `ov_travel_complete` | `openvibe_travel.cpp` | Confirm successful travel |
--
--**Files:**
--- `engine/source-sdk-2013/src/game/server/hl2mp/openvibe_travel.cpp` (8.0 KB)
--- `engine/source-sdk-2013/src/game/server/hl2mp/openvibe_steam_auth.cpp` (8.5 KB)
--
--## Electron Launcher (HTML/CSS/JS)
--
--A custom main menu replacing the stock HL2MP server browser:
--
--**Features:**
--- Portal tab with quick-join buttons for each game mode
--- Live server list (hub, prophunt, deathrun, fortwars, traitortown)
--- Leaderboard, inventory, and shop tabs
--- Neon theme with Rajdhani + Orbitron fonts
--
--**Files:**
--- `launcher/main.js` — Electron main process + Proton spawning
--- `launcher/renderer.js` — UI event handlers
--- `launcher/index.html` — Portal, servers, leaderboard, inventory, shop tabs
--- `launcher/styles.css` — Custom CSS (12 KB)
--- `launcher/assets/` — Images and icons
--
--**How it works:**
--1. Launcher displays server list from `http://localhost:3000/api/servers`
--2. User clicks "Join Hub" or a game mode
--3. Launcher calls `bash tools/run-client-proton.sh <ip> <port>`
--4. Proton launches Windows hl2.exe via Wine + Vulkan/DXVK
--5. Game connects to server with auto-login from backend auth
++Implemented and tested locally:
++
++- **Backend API** - Fastify/TypeScript with dev auth, Steam Web API ticket auth, player profiles, shop, inventory, server registry, travel tokens, match rewards, leaderboard, admin shop management, CDN asset manifest, and optional Redis-backed sessions.
++- **Database** - PostgreSQL migrations for players, inventory, shop, servers, join tokens, currency ledger, and match results.
++- **C++ GameDLL patch** - tracked in `sdk/openvibe/` and applied into the local Valve SDK checkout with `tools/apply-openvibe-sdk.sh`.
++- **Authenticated travel** - `ov_join <mode>` requests `/v1/travel/request`, stores a join token in userinfo, and connects to the selected server.
++- **Server-side arrival validation** - the destination server validates join tokens with `/v1/travel/validate`.
++- **OpenVibe menu hooks** - VGUI commands `openvibe_menu` / `ov_menu` plus `resource/GameMenu.res` expose OpenVibe hub actions instead of relying on the stock server browser.
++- **Prop Hunt C++ disguise command** - `ov_prophunt_disguise <model>` swaps the player to an allowlisted prop model; `ov_prophunt_reset_disguise` restores the player model.
++- **Fort Wars C++ placement command** - `ov_fortwars_spawn <crate|barrel|fence|plate|concrete>` spawns allowlisted physics props during the build phase.
++- **VScript game logic** - Squirrel scripts for hub, Prop Hunt, Deathrun, Fort Wars, and Traitor Town prototypes.
++- **Portal maps** - generated VMFs and compiled BSPs use `ov_join` portals for hub/minigame travel.
++- **Sidecars** - tail SRCDS logs and bridge `[OV]` events to the backend API.
++- **Production infra skeleton** - Docker Compose, Docker Swarm stack, and Kubernetes manifests for API, PostgreSQL, Redis, and CDN/static assets.
++
++Verified locally on June 28, 2026:
++
++- `backend`: TypeScript build passes.
++- `backend`: 9 Vitest tests pass.
++- Source SDK 2013 Linux64 build passes with the OpenVibe C++ patch applied.
++- SRCDS smoke test passes for all five maps: `ov_hub`, `ph_openvibe_dev`, `dr_openvibe_dev`, `fw_openvibe_dev`, `tt_openvibe_dev`.
++- Full dev stack registers all five servers and returns travel targets for hub, Prop Hunt, Deathrun, Fort Wars, and Traitor Town.
++
++## Commands
++
++| Command | Side | Purpose |
++| --- | --- | --- |
++| `ov_join <mode>` | Client | Requests backend travel, stores join token, connects to target server. |
++| `ov_auth_steam` | Client | Requests a Steam Web API ticket and posts it to `/v1/auth/steam`. |
++| `openvibe_menu` / `ov_menu` | Client | Opens the OpenVibe VGUI hub menu. |
++| `ov_open_url <url>` | Client | Opens OpenVibe web surfaces such as auth/inventory in Steam overlay/browser. |
++| `ov_prophunt_disguise <model>` | Server | Applies allowlisted Prop Hunt disguise model. |
++| `ov_prophunt_reset_disguise` | Server | Restores the normal player model. |
++| `ov_fortwars_spawn <type>` | Server | Spawns allowlisted Fort Wars build props. |
## Quick Start
--### Prerequisites
--- Steam logged in with SDK Base 2013 Multiplayer installed
--- GE-Proton10-34 (install via ProtonUp-Qt)
--- Node.js 16+
--- PostgreSQL 12+
--- Wine (for BSP compilation)
--
--### Backend (in-memory, no database needed)
++### Backend
```bash
cd ~/src/openvibe-source/backend
--npm test # 6 tests, should all pass
--npm run build # TypeScript compile
--npm start # Runs on http://localhost:3000
++npm install
++npm run build
++npm test
```
--### Full dev stack (PostgreSQL + API + servers + sidecars)
++### Build Source SDK GameDLLs
```bash
cd ~/src/openvibe-source
--tools/dev-up.sh # starts: postgres → API → SRCDS × 5 → sidecar × 5 (via tmux)
++tools/build-sdk-linux.sh
++tools/setup-openvibe-bin.sh
```
--Or step-by-step:
++`tools/build-sdk-linux.sh` applies `sdk/openvibe/` into `engine/source-sdk-2013/`, patches the VPC files, and builds the HL2MP client/server Linux64 game DLLs.
++
++### Full Dev Stack
```bash
--# 1. Start PostgreSQL
--tools/dev-db-up.sh
++cd ~/src/openvibe-source
++OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh
++```
--# 2. Migrate and run API
--cd backend && npm run migrate && npm run dev
++This starts:
--# 3. Register servers with the API
--cd .. && node tools/register-local-servers.mjs http://127.0.0.1:3000 servers/local-servers.json
++- PostgreSQL
++- OpenVibe API
++- Hub SRCDS
++- Prop Hunt SRCDS
++- Deathrun SRCDS
++- Fort Wars SRCDS
++- Traitor Town SRCDS
++- one sidecar per server
--# 4. Start a server (needs BSPs compiled)
--tools/run-hub.sh
--tools/run-prophunt.sh
++Stop everything:
--# 5. Start sidecars (one per server)
--tools/run-sidecar.sh local-hub-27015 hub 27015 48
--tools/run-sidecar.sh local-prophunt-27016 prophunt 27016 24
++```bash
++cd ~/src/openvibe-source
++tools/dev-down.sh
```
--### Smoke API
++### Smoke Tests
```bash
--cd ~/src/openvibe-source && node tools/smoke-api.mjs
++cd ~/src/openvibe-source
++node tools/smoke-api.mjs
++OPENVIBE_SRCDS_MAP_DELAY=3 OPENVIBE_SRCDS_SMOKE_TIMEOUT=45 tools/smoke-srcds.sh
```
--### Map workflow
++### Maps
```bash
--# Regenerate VMFs (adds VScript entities, HUD text, team spawns)
++cd ~/src/openvibe-source
node tools/generate-dev-vmfs.mjs
--
--# Compile all maps via Wine + TF2 tools
tools/compile-dev-maps-wine.sh
```
--### Client
++Hammer++ is currently launched as a non-Steam game with Proton Experimental. The repo also keeps direct Wine launchers:
```bash
--tools/run-client.sh
++tools/run-hammerpp.sh
++tools/run-hammerpp-wined3d.sh
```
--### Hammer++
++Recommended Hammer++ paths through Wine:
--```bash
--tools/run-hammerpp.sh # Wine + D3D11
--tools/run-hammerpp-wined3d.sh # Wine + WineD3D fallback
++```text
++Game directory:
++Z:\home\workstation\src\openvibe-source\game\openvibe.games
++
++VMF directory:
++Z:\home\workstation\src\openvibe-source\hammer\vmf
++
++Map output:
++Z:\home\workstation\src\openvibe-source\game\openvibe.games\maps
```
--## Architecture
++## Dedicated Server Runtime
--See [`docs/architecture.md`](docs/architecture.md) for the full service diagram.
++The installed Source SDK Base 2013 Dedicated Server path:
--### VScript event protocol
++```text
++/mnt/6tb/ssd_offload/home/workstation/.steam/debian-installation/steamapps/common/Source SDK Base 2013 Dedicated Server
++```
++
++contains Windows server binaries in this environment. The working local Linux64 runtime is the TF2 dedicated server install at:
++
++```text
++~/srcds/tf2
++```
--VScript prints `[OV]`-prefixed lines to the SRCDS console/log.
--The sidecar (`tools/ov-sidecar.mjs`) tails the log file and routes events to the API.
++`tools/run-server.sh` defaults to `~/srcds/tf2` and uses the OpenVibe mod directory at `game/openvibe.games`.
--| Event | Format | Action |
--|-------|--------|--------|
--| `BOOT` | `BOOT serverId mode` | register/re-register server |
--| `HEARTBEAT` | `HEARTBEAT serverId count max state` | update server state |
--| `REWARD` | `REWARD matchId serverId secret uid mode coins xp` | record match reward |
--| `SAY` | `SAY message…` | log message |
++## Architecture
++
++```text
++Source client
++ -> OpenVibe VGUI menu / launcher
++ -> Steam auth ticket or dev auth
++ -> OpenVibe.Games API
++ -> Hub SRCDS
++ -> ov_join portal pads
++ -> Minigame SRCDS shards
++
++OpenVibe.Games API
++ -> PostgreSQL for durable game data
++ -> Redis for sessions when configured
++ -> CDN/static asset manifest for cosmetics
++```
--### API endpoints
++Gameplay remains Source-style authoritative SRCDS instances. The MMO-style part is the shared OpenVibe.Games account, inventory, currency, travel, and server orchestration layer.
++
++## API Endpoints
| Method | Path | Description |
--|--------|------|-------------|
++| --- | --- | --- |
| `GET` | `/health` | Health check |
--| `POST` | `/v1/auth/dev` | Dev player auth (local only) |
--| `POST` | `/v1/auth/steam` | Steam auth (future) |
++| `POST` | `/v1/auth/dev` | Local dev auth |
++| `POST` | `/v1/auth/steam` | Steam Web API ticket auth |
| `GET` | `/v1/me?steamId=` | Player profile + inventory + shop |
| `GET` | `/v1/shop` | Shop catalog |
++| `GET` | `/v1/assets/manifest` | CDN-backed cosmetic asset manifest |
| `POST` | `/v1/shop/buy` | Purchase item |
| `POST` | `/v1/equip` | Equip owned item |
| `GET` | `/v1/leaderboard` | Top players by XP |
@@@ -187,15 +186,31 @@ The sidecar (`tools/ov-sidecar.mjs`) tails the log file and routes events to the
| `POST` | `/v1/matches/end` | Record match result + reward |
| `POST` | `/v1/admin/shop/items` | Admin: upsert shop item |
--## Next Steps
++## Production Configuration
++
++Production auth requires:
++
++- `STEAM_WEB_API_KEY`
++- `STEAM_APP_ID`
++- `OPENVIBE_ADMIN_SECRET`
++- `DATABASE_URL`
++- `SESSION_REDIS_URL`
++- `OPENVIBE_CDN_BASE_URL`
++
++Infrastructure templates live in:
++
++- `infra/docker-compose.yml`
++- `infra/production/docker-stack.yml`
++- `infra/kubernetes/openvibe.yaml`
++- `infra/cdn/nginx.conf`
++
++## Remaining Work
--- **Custom C++ GameDLL** — implement `ov_join <mode>` ConCommand that calls `/v1/travel/request`,
-- receives the connect string + token, and initiates a proper authenticated server connection.
--- **Custom main menu** — VGUI panel or Panorama replacement that shows the OpenVibe hub selection
-- screen instead of the stock HL2MP server browser.
--- **Prop Hunt prop disguise** — C++ player model swap + scale for true prop disguise.
--- **Fort Wars prop placement** — C++ `prop_physics` spawning for the build phase.
--- **Steam auth** — integrate `ISteamUser::AuthenticateUserTicket` for production auth.
--- **CDN / asset hosting** — serve player model and trail assets via openvibe.games CDN.
--- **Production infra** — Kubernetes or Docker Swarm, persistent PostgreSQL, Redis for sessions.
++The foundation is running. The next real work is content and hardening:
++- Replace dev models/trails with real OpenVibe cosmetic assets.
++- Add real hosted CDN storage behind `openvibe.games`.
++- Wire Steam production credentials and app identity.
++- Add party travel, friend invites, and reservation capacity checks.
++- Expand Prop Hunt, Deathrun, Fort Wars, and Traitor Town into full game rules.
++- Add admin moderation, audit views, metrics, backups, and deployment secrets management.
ddiff --git a/backend/.env.example b/backend/.env.example
iindex bf048ce..dacc913 100644
---- a/backend/.env.example
++++ b/backend/.env.example
@@@ -2,5 +2,11 @@ HOST=127.0.0.1
PORT=3000
DATABASE_URL=postgres://openvibe:[email protected]:5432/openvibe
OPENVIBE_PUBLIC_URL=https://openvibe.games
++OPENVIBE_CDN_BASE_URL=https://openvibe.games/cdn
OPENVIBE_DEV_AUTH_ENABLED=true
OPENVIBE_DEFAULT_SERVER_SECRET=dev-secret
++OPENVIBE_ADMIN_SECRET=change-me
++SESSION_REDIS_URL=redis://127.0.0.1:6379
++STEAM_APP_ID=243750
++STEAM_WEB_API_KEY=
++STEAM_WEB_API_BASE=https://api.steampowered.com
ddiff --git a/backend/package-lock.json b/backend/package-lock.json
iindex 2ee16ae..162a255 100644
---- a/backend/package-lock.json
++++ b/backend/package-lock.json
@@@ -11,6 +11,7 @@
"@fastify/cors": "^11.1.0",
"dotenv": "^17.2.3",
"fastify": "^5.6.2",
++ "ioredis": "^5.11.1",
"nanoid": "^5.1.6",
"pg": "^8.16.3",
"zod": "^4.2.1"
@@@ -633,6 +634,12 @@
"ipaddr.js": "^2.1.0"
}
},
++ "node_modules/@ioredis/commands": {
++ "version": "1.10.0",
++ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz",
++ "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==",
++ "license": "MIT"
++ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@@ -1223,6 +1230,15 @@
"node": ">=18"
}
},
++ "node_modules/cluster-key-slot": {
++ "version": "1.1.1",
++ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz",
++ "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==",
++ "license": "Apache-2.0",
++ "engines": {
++ "node": ">=0.10.0"
++ }
++ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@@ -1243,6 +1259,32 @@
"url": "https://opencollective.com/express"
}
},
++ "node_modules/debug": {
++ "version": "4.4.3",
++ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
++ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
++ "license": "MIT",
++ "dependencies": {
++ "ms": "^2.1.3"
++ },
++ "engines": {
++ "node": ">=6.0"
++ },
++ "peerDependenciesMeta": {
++ "supports-color": {
++ "optional": true
++ }
++ }
++ },
++ "node_modules/denque": {
++ "version": "2.1.0",
++ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
++ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
++ "license": "Apache-2.0",
++ "engines": {
++ "node": ">=0.10"
++ }
++ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@@ -1509,6 +1551,28 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
++ "node_modules/ioredis": {
++ "version": "5.11.1",
++ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz",
++ "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==",
++ "license": "MIT",
++ "dependencies": {
++ "@ioredis/commands": "1.10.0",
++ "cluster-key-slot": "1.1.1",
++ "debug": "4.4.3",
++ "denque": "2.1.0",
++ "redis-errors": "1.2.0",
++ "redis-parser": "3.0.0",
++ "standard-as-callback": "2.1.0"
++ },
++ "engines": {
++ "node": ">=12.22.0"
++ },
++ "funding": {
++ "type": "opencollective",
++ "url": "https://opencollective.com/ioredis"
++ }
++ },
"node_modules/ipaddr.js": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
@@@ -1863,6 +1927,12 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
++ "node_modules/ms": {
++ "version": "2.1.3",
++ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
++ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
++ "license": "MIT"
++ },
"node_modules/nanoid": {
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
@@@ -2175,6 +2245,27 @@
"node": ">= 12.13.0"
}
},
++ "node_modules/redis-errors": {
++ "version": "1.2.0",
++ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
++ "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
++ "license": "MIT",
++ "engines": {
++ "node": ">=4"
++ }
++ },
++ "node_modules/redis-parser": {
++ "version": "3.0.0",
++ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
++ "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
++ "license": "MIT",
++ "dependencies": {
++ "redis-errors": "^1.0.0"
++ },
++ "engines": {
++ "node": ">=4"
++ }
++ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@@ -2350,6 +2441,12 @@
"dev": true,
"license": "MIT"
},
++ "node_modules/standard-as-callback": {
++ "version": "2.1.0",
++ "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
++ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
++ "license": "MIT"
++ },
"node_modules/std-env": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
ddiff --git a/backend/package.json b/backend/package.json
iindex cf99c02..c4f8a29 100644
---- a/backend/package.json
++++ b/backend/package.json
@@@ -16,6 +16,7 @@
"@fastify/cors": "^11.1.0",
"dotenv": "^17.2.3",
"fastify": "^5.6.2",
++ "ioredis": "^5.11.1",
"nanoid": "^5.1.6",
"pg": "^8.16.3",
"zod": "^4.2.1"
ddiff --git a/backend/src/app.test.ts b/backend/src/app.test.ts
iindex 91521aa..5779b8f 100644
---- a/backend/src/app.test.ts
++++ b/backend/src/app.test.ts
@@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { createApp } from "./app.js";
import { MemoryOpenVibeRepository } from "./repository-memory.js";
++import { SessionInput } from "./sessions.js";
const steamId = "76561198000000000";
const serverSecret = "dev-secret";
@@@ -33,6 +34,73 @@ describe("OpenVibe API vertical slice", () => {
await app.close();
});
++ it("writes auth sessions to the configured session store", async () => {
++ const sessions: SessionInput[] = [];
++ const app = await createApp({
++ repository: new MemoryOpenVibeRepository(),
++ devAuthEnabled: true,
++ sessionStore: {
++ async createSession(input) {
++ sessions.push(input);
++ },
++ },
++ });
++ await app.ready();
++
++ const response = await app.inject({
++ method: "POST",
++ url: "/v1/auth/dev",
++ payload: { steamId, displayName: "Session Tester" },
++ });
++
++ expect(response.statusCode).toBe(200);
++ expect(response.json().sessionToken).toMatch(/^dev\.76561198000000000\./);
++ expect(sessions).toHaveLength(1);
++ expect(sessions[0]).toMatchObject({
++ steamId,
++ provider: "dev",
++ ttlSeconds: 86400,
++ });
++
++ await app.close();
++ });
++
++ it("keeps Steam auth disabled until Steam credentials are configured", async () => {
++ const app = await testApp();
++
++ const response = await app.inject({
++ method: "POST",
++ url: "/v1/auth/steam",
++ payload: {
++ ticket: "0123456789abcdef",
++ identity: "openvibe.games",
++ },
++ });
++
++ expect(response.statusCode).toBe(501);
++ expect(response.json().error).toBe("steam_auth_not_configured");
++
++ await app.close();
++ });
++
++ it("returns a CDN asset manifest for shop-backed cosmetics", async () => {
++ const app = await testApp();
++
++ const response = await app.inject({
++ method: "GET",
++ url: "/v1/assets/manifest",
++ });
++
++ expect(response.statusCode).toBe(200);
++ const body = response.json();
++ expect(body.cdnBaseUrl).toBe("https://openvibe.games/cdn");
++ expect(body.assets.some((asset: { itemId: string; url: string }) =>
++ asset.itemId === "trail_blue" && asset.url.includes("https://openvibe.games/cdn/"),
++ )).toBe(true);
++
++ await app.close();
++ });
++
it("registers servers and reserves a one-use travel token", async () => {
const app = await testApp();
ddiff --git a/backend/src/app.ts b/backend/src/app.ts
iindex 6e5f45d..66df5df 100644
---- a/backend/src/app.ts
++++ b/backend/src/app.ts
@@@ -4,6 +4,7 @@ import { ZodError } from "zod";
import { OpenVibeRepository } from "./domain.js";
import {
authDevSchema,
++ authSteamSchema,
buyItemSchema,
equipItemSchema,
getMeQuerySchema,
@@@ -17,11 +18,13 @@ import {
validateJoinTokenSchema,
} from "./schemas.js";
import { RepositoryError } from "./repository-memory.js";
++import { createSessionToken, OpenVibeSessionStore } from "./sessions.js";
export interface AppOptions {
repository: OpenVibeRepository;
devAuthEnabled?: boolean;
adminSecret?: string;
++ sessionStore?: OpenVibeSessionStore;
}
export async function createApp(options: AppOptions): Promise<FastifyInstance> {
@@@ -78,18 +81,79 @@ export async function createApp(options: AppOptions): Promise<FastifyInstance> {
const body = authDevSchema.parse(request.body ?? {});
const profile = await options.repository.upsertDevPlayer(body);
++ const sessionToken = await createSessionToken(options.sessionStore, "dev", profile.player.steamId);
return {
-- sessionToken: `dev.${profile.player.steamId}`,
++ sessionToken,
...profile,
};
});
-- app.post("/v1/auth/steam", async (_request, reply) => {
-- return reply.code(501).send({
-- error: "steam_auth_not_configured",
-- next: "Use ISteamUser::GetAuthTicketForWebApi client-side, then verify with AuthenticateUserTicket server-side.",
++ app.post("/v1/auth/steam", async (request, reply) => {
++ const body = authSteamSchema.parse(request.body ?? {});
++ const apiKey = process.env.STEAM_WEB_API_KEY;
++ const appId = process.env.STEAM_APP_ID;
++ const steamApiBase = process.env.STEAM_WEB_API_BASE ?? "https://api.steampowered.com";
++
++ if (!apiKey || !appId) {
++ return reply.code(501).send({
++ error: "steam_auth_not_configured",
++ next: "Set STEAM_WEB_API_KEY and STEAM_APP_ID so the backend can call ISteamUserAuth/AuthenticateUserTicket.",
++ });
++ }
++
++ const url = new URL("/ISteamUserAuth/AuthenticateUserTicket/v1/", steamApiBase);
++ url.searchParams.set("key", apiKey);
++ url.searchParams.set("appid", appId);
++ url.searchParams.set("ticket", body.ticket);
++ url.searchParams.set("identity", body.identity);
++ url.searchParams.set("format", "json");
++
++ const steamResponse = await fetch(url);
++ if (!steamResponse.ok) {
++ request.log.warn({ status: steamResponse.status }, "steam auth request failed");
++ return reply.code(502).send({ error: "steam_auth_upstream_failed" });
++ }
++
++ const steamJson = (await steamResponse.json()) as {
++ response?: {
++ params?: {
++ steamid?: string;
++ ownersteamid?: string;
++ vacbanned?: boolean;
++ publisherbanned?: boolean;
++ };
++ error?: {
++ errorcode?: number;
++ errordesc?: string;
++ };
++ };
++ };
++
++ const params = steamJson.response?.params;
++ if (!params?.steamid) {
++ request.log.warn({ steam: steamJson.response?.error }, "steam auth ticket rejected");
++ return reply.code(401).send({ error: "steam_ticket_invalid" });
++ }
++
++ if (params.vacbanned || params.publisherbanned) {
++ return reply.code(403).send({ error: "steam_account_banned" });
++ }
++
++ const displayName = body.displayName ?? `Steam ${params.steamid.slice(-6)}`;
++ const profile = await options.repository.upsertDevPlayer({
++ steamId: params.steamid,
++ displayName,
});
++ const sessionToken = await createSessionToken(options.sessionStore, "steam", params.steamid);
++
++ return {
++ authenticated: true,
++ sessionToken,
++ steamId: params.steamid,
++ ownerSteamId: params.ownersteamid ?? params.steamid,
++ ...profile,
++ };
});
app.get("/v1/me", async (request, reply) => {
@@@ -103,6 +167,29 @@ export async function createApp(options: AppOptions): Promise<FastifyInstance> {
items: await options.repository.listShop(),
}));
++ app.get("/v1/assets/manifest", async () => {
++ const cdnBaseUrl = (process.env.OPENVIBE_CDN_BASE_URL ?? "https://openvibe.games/cdn").replace(/\/+$/, "");
++ const shop = await options.repository.listShop();
++
++ return {
++ cdnBaseUrl,
++ generatedAt: new Date().toISOString(),
++ assets: shop
++ .filter((item) => item.assetPath.length > 0)
++ .map((item) => {
++ const isAbsolute = /^https?:\/\//i.test(item.assetPath);
++ const assetPath = item.assetPath.replace(/^\/+/, "");
++ return {
++ itemId: item.itemId,
++ itemType: item.itemType,
++ displayName: item.displayName,
++ assetPath: item.assetPath,
++ url: isAbsolute ? item.assetPath : `${cdnBaseUrl}/${assetPath}`,
++ };
++ }),
++ };
++ });
++
app.post("/v1/shop/buy", async (request) => {
const body = buyItemSchema.parse(request.body);
return options.repository.buyItem(body);
@@@ -170,5 +257,9 @@ export async function createApp(options: AppOptions): Promise<FastifyInstance> {
return options.repository.upsertShopItem(body);
});
++ app.addHook("onClose", async () => {
++ await options.sessionStore?.close?.();
++ });
++
return app;
}
ddiff --git a/backend/src/index.ts b/backend/src/index.ts
iindex d4629ff..192be27 100644
---- a/backend/src/index.ts
++++ b/backend/src/index.ts
@@@ -2,6 +2,7 @@ import "dotenv/config";
import { Pool } from "pg";
import { createApp } from "./app.js";
import { PgOpenVibeRepository } from "./repository-pg.js";
++import { RedisSessionStore } from "./sessions.js";
const pool = new Pool({
connectionString:
@@@ -11,6 +12,9 @@ const pool = new Pool({
const app = await createApp({
repository: new PgOpenVibeRepository(pool),
devAuthEnabled: process.env.OPENVIBE_DEV_AUTH_ENABLED !== "false",
++ sessionStore: process.env.SESSION_REDIS_URL
++ ? new RedisSessionStore(process.env.SESSION_REDIS_URL)
++ : undefined,
});
const host = process.env.HOST ?? "127.0.0.1";
ddiff --git a/backend/src/schemas.ts b/backend/src/schemas.ts
iindex 7c77b9b..fb977ba 100644
---- a/backend/src/schemas.ts
++++ b/backend/src/schemas.ts
@@@ -11,6 +11,12 @@ export const authDevSchema = z.object({
displayName: z.string().trim().min(1).max(64).default("OpenVibe Dev"),
});
++export const authSteamSchema = z.object({
++ ticket: z.string().regex(/^[0-9a-fA-F]+$/).min(16).max(8192),
++ identity: z.string().trim().min(1).max(128).default("openvibe.games"),
++ displayName: z.string().trim().min(1).max(64).optional(),
++});
++
export const getMeQuerySchema = z.object({
steamId,
});
ddiff --git a/backend/src/sessions.ts b/backend/src/sessions.ts
nnew file mode 100644
iindex 0000000..eb30e0c
---- /dev/null
++++ b/backend/src/sessions.ts
@@@ -0,0 +1,65 @@
++import { nanoid } from "nanoid";
++import { Redis } from "ioredis";
++
++export interface SessionInput {
++ token: string;
++ steamId: string;
++ provider: "dev" | "steam";
++ ttlSeconds: number;
++}
++
++export interface OpenVibeSessionStore {
++ createSession(input: SessionInput): Promise<void>;
++ close?(): Promise<void>;
++}
++
++export async function createSessionToken(
++ store: OpenVibeSessionStore | undefined,
++ provider: "dev" | "steam",
++ steamId: string,
++): Promise<string> {
++ const token = `${provider}.${steamId}.${nanoid(32)}`;
++
++ if (store) {
++ await store.createSession({
++ token,
++ steamId,
++ provider,
++ ttlSeconds: 60 * 60 * 24,
++ });
++ }
++
++ return token;
++}
++
++export class RedisSessionStore implements OpenVibeSessionStore {
++ private readonly redis: Redis;
++
++ constructor(url: string) {
++ this.redis = new Redis(url, {
++ lazyConnect: true,
++ maxRetriesPerRequest: 2,
++ });
++ }
++
++ async createSession(input: SessionInput): Promise<void> {
++ if (this.redis.status === "wait") {
++ await this.redis.connect();
++ }
++
++ await this.redis.set(
++ `openvibe:session:${input.token}`,
++ JSON.stringify({
++ steamId: input.steamId,
++ provider: input.provider,
++ createdAt: new Date().toISOString(),
++ }),
++ "EX",
++ input.ttlSeconds,
++ );
++ }
++
++ async close(): Promise<void> {
++ this.redis.disconnect();
++ }
++}
ddiff --git a/docs/PHASE_2_SUMMARY.md b/docs/PHASE_2_SUMMARY.md
iindex 71a6222..6669eca 100644
---- a/docs/PHASE_2_SUMMARY.md
++++ b/docs/PHASE_2_SUMMARY.md
@@@ -1,396 +1,134 @@
--# OpenVibe: Source - Final Phase Implementation Summary
--
--## Overview
--
--Completed the final phase of OpenVibe: Source development, implementing all remaining C++ GameDLL features, client UI, server integration, and comprehensive Linux deployment guide.
--
--## Work Completed
--
--### 1. **Custom C++ GameDLL for Authenticated Travel** ✅
--**File:** `engine/source-sdk-2013/src/game/server/hl2mp/openvibe_travel.cpp` (8.1 KB)
--
--**Features:**
--- `ov_join <mode>` ConCommand for authenticated server travel
--- Travel request HTTP callback handling with token validation
--- `ov_travel_complete` ConCommand for server-to-server handoff
--- `ov_prophunt_disguise <model>` for prop disguise mechanics
--- Validated prop model list (10 physics props)
--- Integration with `/v1/travel/request` API endpoint
--
--**Key Classes:**
--- `TravelContext` struct tracks pending travel requests with 30s expiration
--- OnTravelResponse() parses JSON responses and extracts `connect` + `joinToken`
--- Full model swap with weapon removal and name hiding for prop disguise
--
--**ConVars:**
--- `ov_api_endpoint`: API server URL
--- `ov_server_id`: Server instance identifier
--- `ov_server_secret`: Server authentication secret
--
-----
--
--### 2. **Custom VGUI Main Menu Panel** ✅
--**File:** `engine/source-sdk-2013/src/game/client/hl2mp/vgui_openvibe_menu.cpp` (9.9 KB)
--
--**Features:**
--- `COpenVibeMainMenu` frame replaces default HL2MP server browser
--- 5 game mode portal buttons (Hub, Prop Hunt, Deathrun, Fort Wars, Traitor Town)
--- Tab-based interface: Portal, Shop, Leaderboard, Inventory
--- Each tab switches panel visibility
--- Portal buttons emit `ov_join <mode>` ConCommands
--
--**UI Components:**
--- Navigation bar with 5 tab buttons
--- Portal panel with large mode buttons (250x60 pixels each)
--- Shop panel with cosmetic descriptions
--- Leaderboard panel (placeholder for async data loading)
--- Inventory panel (placeholder for user items)
--
--**Factory Functions:**
--- `OpenVibeMenuCreate(Panel* parent)`: Creates singleton menu instance
--- `OpenVibeMenuDestroy()`: Cleans up and marks for deletion
--
-----
--
--### 3. **Steam Authentication Module** ✅
--**File:** `engine/source-sdk-2013/src/game/server/hl2mp/openvibe_steam_auth.cpp` (8.7 KB)
--
--**Features:**
--- `CSteamAuthHandler` singleton manages auth state for all players
--- `BeginPlayerAuth()` validates Steam auth tickets cryptographically
--- Steam GameServer::BeginAuthSession integration
--- Per-player auth context tracking with 60s expiration
--- `ov_auth_ticket <hex_ticket>` ConCommand from client
--- `ov_auth_confirm <token>` ConCommand for validation
--- `OnSteamAuthSessionTicketResponse` callback handles Steam responses
--
--**Auth Flow:**
--1. Client calls `ov_auth_ticket` with Steam-provided auth ticket
--2. Server validates with `ISteamGameServer::BeginAuthSession`
--3. Server sends confirmation with token to client
--4. Client calls `ov_auth_confirm` with token
--5. Server validates token matches and hasn't expired
--6. Session marked authenticated, resources granted
--
--**Steam Response Handling:**
--- k_EAuthSessionResponseOK: Valid session
--- k_EAuthSessionResponseVACBanned: VAC ban detection
--- k_EAuthSessionResponseLoggedInElsewhere: Session hijack detection
--- All other responses logged with player name and SteamID
--
-----
--
--### 4. **BSP Compilation Script** ✅
--**File:** `tools/compile-dev-maps-wine.sh` (Enhanced)
--
--**Features:**
--- Prerequisites checking (Wine, Node.js, TF2 installation)
--- Automated VMF generation via `generate-dev-vmfs.mjs`
--- VBSP (geometry) → VVIS (visibility, optional) → VRAD (lighting) pipeline
--- All 5 maps compiled in sequence:
-- - ov_hub
-- - ph_openvibe_dev
-- - dr_openvibe_dev
-- - fw_openvibe_dev
-- - tt_openvibe_dev
--
--**Configuration Options:**
--- `TF2_INSTALL_DIR`: Override TF2 path
--- `VVIS_ENABLED=0`: Skip slow VVIS optimization
--- Wine 32-bit architecture (`WINEARCH=win32`)
--
--**Output:**
--- Compiled BSP files in `game/openvibe.games/maps/*.bsp`
--- Ready for SRCDS deployment
--
-----
--
--### 5. **Comprehensive Linux Setup & Run Guide** ✅
--**File:** `LINUX_SETUP.md` (12.2 KB)
--
--**Sections:**
--
--#### Prerequisites (System Requirements)
--- Ubuntu 20.04+ with 50GB free space
--- 8-16GB RAM for multiple servers
--- All required packages listed with apt-get commands
--
--#### Software Installation (5-Part Setup)
--1. System dependencies (build tools, Wine, Node, PostgreSQL, Docker)
--2. Steam + SteamCMD for game installations
--3. Team Fortress 2 (for BSP tools)
--4. HL2: Deathmatch (game base)
--5. OpenVibe repository clone
--
--#### Configuration (4 Parts)
--1. Environment variables (.bashrc)
--2. SRCDS directory structure
--3. PostgreSQL database + schema init
--4. Backend .env with database credentials
--
--#### Running Servers (4 Methods)
--1. Start API backend (Node.js or Docker)
--2. Start all servers via tmux session
--3. Manual single-server startup with SRCDS
--4. Client connection with Proton Experimental
--
--#### Comprehensive Troubleshooting
--- Game server startup issues
--- API connection problems
--- BSP compilation errors
--- VScript debugging
--- Player connection issues
--- Firewall configuration
--
--#### Development Workflow
--- VScript modification (no restart needed)
--- Backend API changes with rebuild
--- Map updates with compilation
--
--#### Monitoring & Logs
--- Server status checks
--- Log file locations
--- Performance tuning recommendations
--- Database optimization
--
--#### Production Deployment
--- Environment-specific configs
--- Docker service setup
--- SSL/TLS reverse proxy
--- Monitoring setup
--
--#### Useful Commands
--- tmux session management
--- API endpoint queries (curl examples)
--- Shop item creation
--- Debug package generation
--
-----
--
--## File Summary
--
--| File | Size | Purpose |
--|------|------|---------|
--| openvibe_travel.cpp | 8.1 KB | Server travel system + prop disguise |
--| vgui_openvibe_menu.cpp | 9.9 KB | Client main menu VGUI |
--| openvibe_steam_auth.cpp | 8.7 KB | Server-side Steam authentication |
--| compile-dev-maps-wine.sh | ~5 KB | Map compilation pipeline |
--| LINUX_SETUP.md | 12.2 KB | Complete deployment guide |
--
--**Total New Code: ~44 KB**
--
-----
--
--## Integration Points
--
--### C++ ↔ API
--- `ov_join <mode>` calls `/v1/travel/request` via HTTP
--- Backend responds with `{connect, joinToken}`
--- Server uses token for authenticated server transfer
--
--### C++ ↔ VScript
--- VScript already implements per-mode game logic
--- Per-map CFGs auto-exec VScript files
--- C++ ConCommands can trigger VScript functions via script engine
--
--### C++ ↔ Steam
--- Client provides auth ticket to server
--- Server validates with Steam GameServer API
--- Player marked authenticated for gameplay
--
--### Client ↔ Server Travel Flow
++# OpenVibe: Source Implementation Summary
++
++## Scope
++
++This phase moved OpenVibe from VScript/backend prototype into a compiled Source SDK foundation:
++
++- authenticated travel command
++- custom VGUI menu command
++- server-side join-token validation
++- Prop Hunt disguise hook
++- Fort Wars prop placement hook
++- Steam Web API auth route
++- Redis session support
++- CDN asset manifest
++- production infra templates
++- SRCDS smoke-tested maps
++
++## C++ GameDLL Patch
++
++Tracked source files:
++
++```text
++sdk/openvibe/client/hl2mp/openvibe_client.cpp
++sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp
++sdk/openvibe/server/hl2mp/openvibe_server.cpp
```
--Client Menu
-- → [Hub Button]
-- → ov_join hub
-- → Server API: /v1/travel/request
-- → Response: {connect: "127.0.0.1:27016", joinToken: "..."}
-- → engine->ClientCommand("connect 127.0.0.1:27016; ov_auth TOKEN")
-- → New Server: ov_travel_complete TOKEN
-- → Validation + welcome
++
++Apply/build tooling:
++
++```text
++tools/apply-openvibe-sdk.sh
++tools/build-sdk-linux.sh
++tools/setup-openvibe-bin.sh
```
-----
--
--## Build Instructions
--
--### For Mod Developers
--
--1. **Set up Source SDK 2013 build environment:**
-- ```bash
-- cd engine/source-sdk-2013
-- ./createsln.bat
-- # Open createsln_hl2mp.sln in Visual Studio
-- ```
--
--2. **Add new files to project:**
-- - Right-click `hl2mp_dll` project → Add → Existing Item
-- - Add `openvibe_travel.cpp` (server)
-- - Add `openvibe_steam_auth.cpp` (server)
-- - Add `vgui_openvibe_menu.cpp` (client)
--
--3. **Link Steam API:**
-- - Project Properties → Linker → Input
-- - Add: `steam_api.lib`
--
--4. **Compile:**
-- - Build → Build Solution (Release configuration)
-- - Output: `mod_hl2mp.dll`
--
--5. **Deploy:**
-- ```bash
-- cp bin/mod_hl2mp.dll $SRCDS_DIR/hlserver/orangebox/hl2mp/bin/
-- ```
--
-----
--
--## Feature Completeness
--
--### Implemented ✅
--- [x] ov_join ConCommand with API integration
--- [x] ov_travel_complete token validation
--- [x] ov_prophunt_disguise with prop models
--- [x] Custom VGUI main menu (5 tabs)
--- [x] Portal buttons for all game modes
--- [x] Steam auth ticket validation
--- [x] BeginAuthSession / EndAuthSession
--- [x] Auth context tracking with expiry
--- [x] BSP compilation (VBSP, VVIS, VRAD)
--- [x] Full Linux deployment guide
--- [x] Prerequisites checking
--- [x] Database setup instructions
--- [x] Server launch procedures
--- [x] Troubleshooting section
--- [x] Production deployment guide
--
--### Future Enhancements
--- [ ] Async HTTP client integration in C++ (currently pseudo-code comments)
--- [ ] VScript ↔ C++ bidirectional API for advanced mechanics
--- [ ] Anti-cheat integration (FaceIT, EAC)
--- [ ] Discord bot integration for server status
--- [ ] Web dashboard for player stats
--- [ ] Matchmaking algorithm for skill-based teams
--- [ ] Replay system for highlights
--- [ ] Custom weapon models/skins
--
-----
--
--## Testing Checklist
--
--### Before Deployment
--- [ ] Compile all C++ modules without errors
--- [ ] Load game with new DLL
--- [ ] Test `ov_join hub` ConCommand
--- [ ] Test `ov_prophunt_disguise` with valid/invalid models
--- [ ] Test Steam auth flow (ticket → confirm → authenticate)
--- [ ] Open main menu VGUI panel
--- [ ] Test tab switching (Portal → Shop → Leaderboard)
--- [ ] Click Portal buttons and verify travel requests
--- [ ] Compile BSPs successfully (VBSP + VRAD)
--- [ ] Run Linux setup guide start-to-finish
--
--### Runtime
--- [ ] Player connects and authenticates
--- [ ] Player can travel between servers
--- [ ] Prop Hunt players disguise correctly
--- [ ] Main menu panels appear correctly
--- [ ] Leaderboard data loads from API
--- [ ] Server logs show [OV] events
--- [ ] Sidecar bridges events to API
--- [ ] Match rewards recorded idempotently
--
-----
--
--## Performance Notes
--
--### C++ Memory
--- `TravelContext`: ~1KB per pending request (max 10 concurrent)
--- `CSteamAuthHandler::m_pendingAuths`: ~2KB per auth session (max 32 concurrent)
--- VGUI panel: ~50KB base, ~1KB per active control
--
--### Network
--- Travel request: ~200 bytes HTTP POST
--- Auth ticket: ~128 bytes typical
--- Join token response: ~256 bytes JSON
--
--### Compilation Time
--- VBSP: ~30-60s per map
--- VVIS: ~5-30min per map (optional)
--- VRAD: ~60-300s per map (depends on lighting complexity)
--
-----
--
--## Next Steps for User
--
--1. **Integrate C++ code into Source SDK build:**
-- - Copy .cpp files to mod_hl2mp project
-- - Update project files
-- - Compile and test
--
--2. **Follow Linux Setup guide:**
-- - Run through full prerequisites
-- - Initialize database
-- - Build backend
-- - Compile maps
--
--3. **Launch dev environment:**
-- ```bash
-- ./tools/dev-up.sh
-- ```
--
--4. **Connect client and test gameplay**
--
--5. **Deploy to production infrastructure**
--
-----
--
--## Files Modified/Created in Phase 2
--
--### New Files Created
--- `engine/source-sdk-2013/src/game/server/hl2mp/openvibe_travel.cpp`
--- `engine/source-sdk-2013/src/game/client/hl2mp/vgui_openvibe_menu.cpp`
--- `engine/source-sdk-2013/src/game/server/hl2mp/openvibe_steam_auth.cpp`
--- `LINUX_SETUP.md`
--
--### Files Modified
--- `tools/compile-dev-maps-wine.sh` (enhanced with error checking)
--
-----
--
--## Conclusion
--
--OpenVibe: Source now has a complete implementation stack:
--
--**Backend Layer** (Phase 1)
--- Fastify API with authentication, leaderboard, shop endpoints
--- PostgreSQL database with player profiles, matches, inventory
--- Docker infrastructure for production deployment
--
--**Game Logic Layer** (Phase 1)
--- VScript for all 5 game modes with state machines
--- Per-map auto-exec CFGs for VScript loading
--- Sidecar process to bridge SRCDS logs to API
++The patch is copied into the ignored local Valve SDK checkout at `engine/source-sdk-2013/` and added to the HL2MP client/server VPC projects before building.
++
++### Client Commands
--**Client Layer** (Phase 2)
--- Custom main menu VGUI replacing server browser
--- Portal buttons for quick mode selection
--- Authenticated travel ConCommand
++| Command | Purpose |
++| --- | --- |
++| `ov_join <mode>` | Calls `/v1/travel/request`, stores join token, connects to returned server. |
++| `ov_auth_steam` | Gets a Steam Web API ticket and posts to `/v1/auth/steam`. |
++| `openvibe_menu` / `ov_menu` | Opens the OpenVibe VGUI hub selector. |
++| `ov_open_url <url>` | Opens OpenVibe web pages such as auth/inventory. |
++
++### Server Commands / Hooks
++
++| Command / hook | Purpose |
++| --- | --- |
++| `ClientActive` hook | Reads `ov_join_token` and validates it against the backend. |
++| `ov_prophunt_disguise <model>` | Applies an allowlisted Prop Hunt disguise model. |
++| `ov_prophunt_reset_disguise` | Restores the normal player model. |
++| `ov_fortwars_spawn <type>` | Spawns allowlisted physics props for Fort Wars build phase. |
++
++## Backend Work
++
++Implemented in `backend/src/`:
++
++- `/v1/auth/steam` validates Steam Web API tickets when `STEAM_WEB_API_KEY` and `STEAM_APP_ID` are configured.
++- Session tokens use Redis when `SESSION_REDIS_URL` is configured, otherwise local/dev behavior remains available.
++- `/v1/assets/manifest` exposes CDN URLs for shop assets using `OPENVIBE_CDN_BASE_URL`.
++- Travel, validation, rewards, shop, equip, leaderboard, and admin item flows are covered by tests.
++
++Verification:
++
++```text
++npm run build
++npm test
++```
++
++Current result: TypeScript build passes and 9 tests pass.
++
++## Maps And SRCDS
++
++The VMF generator now emits portal commands using `ov_join`:
++
++```text
++ov_join hub
++ov_join prophunt
++ov_join deathrun
++ov_join fortwars
++ov_join traitortown
++```
++
++Compiled and smoke-tested BSPs:
++
++```text
++game/openvibe.games/maps/ov_hub.bsp
++game/openvibe.games/maps/ph_openvibe_dev.bsp
++game/openvibe.games/maps/dr_openvibe_dev.bsp
++game/openvibe.games/maps/fw_openvibe_dev.bsp
++game/openvibe.games/maps/tt_openvibe_dev.bsp
++```
++
++SRCDS verification:
++
++```bash
++OPENVIBE_SRCDS_MAP_DELAY=3 OPENVIBE_SRCDS_SMOKE_TIMEOUT=45 tools/smoke-srcds.sh
++```
++
++Current result: all five maps reach server activation without fatal load errors or VScript startup errors.
++
++## Infrastructure
++
++Added or expanded:
++
++```text
++infra/docker-compose.yml
++infra/cdn/nginx.conf
++infra/production/docker-stack.yml
++infra/kubernetes/openvibe.yaml
++```
--**Infrastructure** (Phase 2)
--- C++ GameDLL with travel, auth, disguise mechanics
--- Steam authentication integration
--- Map compilation pipeline (VBSP/VVIS/VRAD)
--- Comprehensive Linux deployment guide with troubleshooting
++These provide the production shape for:
--**Ready for deployment on Linux with:**
--- Full prerequisite checking
--- Database initialization
--- Multi-server tmux sessions
--- API/database monitoring
--- Production configuration templates
++- API
++- PostgreSQL
++- Redis
++- CDN/static assets
++- Kubernetes services/ingress/secrets
++- Docker Swarm deployment
-----
++## Remaining Work
--**Total Development**: 40+ game modes/mechanics, 50k+ LOC VScript, 15k+ LOC Node.js, 20k+ LOC TypeScript, 20k+ LOC C++, 12k+ documentation.
++This is a working local MVP foundation, not a finished public game. Production still needs:
--**Status**: ✅ **READY FOR DEPLOYMENT**
++- real Steam app credentials
++- real hosted assets under `openvibe.games`
++- production secrets and backups
++- metrics/logging/alerts
++- larger maps and polished assets
++- completed game-mode rules
++- moderation and anti-abuse controls
ddiff --git a/docs/architecture.md b/docs/architecture.md
iindex 3f0885b..6761e32 100644
---- a/docs/architecture.md
++++ b/docs/architecture.md
@@@ -6,12 +6,12 @@ OpenVibe: Source uses Source dedicated servers for gameplay and OpenVibe.Games a
```text
Source client
-- → OpenVibe main menu
++ → OpenVibe VGUI menu / launcher
→ Steam auth / dev auth → OpenVibe.Games API
-- → Hub SRCDS (portal pads → direct connect)
++ → Hub SRCDS (portal pads → ov_join)
Hub SRCDS ov_hub.nut
-- → portal pads (trigger → connect)
++ → portal pads (trigger → ov_join <mode>)
→ shop NPC / inventory station
→ sidecar → /v1/servers/heartbeat
@@@ -30,6 +30,7 @@ Sidecar (ov-sidecar.mjs)
Backend API (Fastify/TypeScript)
→ /v1/auth/{dev,steam}
→ /v1/me /v1/shop /v1/equip
++ → /v1/assets/manifest
→ /v1/servers/{register,heartbeat}
→ /v1/travel/{request,validate}
→ /v1/matches/end
@@@ -41,6 +42,12 @@ PostgreSQL
→ shop_items, player_items
→ game_servers, join_tokens
→ match_results
++
++Redis (optional)
++ → auth sessions
++
++CDN/static hosting
++ → cosmetic assets served under openvibe.games
```
## Modes
@@@ -61,6 +68,7 @@ PostgreSQL
- Server heartbeat and reward APIs require `serverSecret`.
- The backend owns item IDs, asset paths, prices, and equipped cosmetics.
- Admin endpoints require `X-Admin-Secret` header.
++- Production Steam auth is done by the backend calling Steam Web API ticket validation. The client never receives the Steam Web API publisher key.
## VScript → API Event Protocol
@@@ -77,26 +85,29 @@ VScript cannot make HTTP requests directly. The sidecar bridges the gap:
| `REWARD` | `matchId serverId secret uid mode coins xp` | `POST /v1/matches/end` |
| `SAY` | `message…` | log only |
--## Next C++ Work
++## C++ Travel Flow
--The current portal pads use `point_clientcommand` with hardcoded `connect` commands.
--Replace this with a proper authenticated travel flow:
++Portal pads now call `ov_join <mode>` instead of hardcoded `connect` commands:
```text
--1. Player steps on portal pad → VScript prints [OV] TRAVEL_REQUEST steamId mode
--2. Sidecar calls /v1/travel/request → gets { connect, joinToken }
--3. Sidecar sends RCON command: ov_send_travel {userId} {connect} {joinToken}
--4. C++ ConCommand parses the command, sends connect + token to the client
--5. Destination server receives player + token
--6. Server VScript prints [OV] ARRIVAL steamId token
--7. Sidecar calls /v1/travel/validate → confirms token is valid
++1. Player steps on portal pad.
++2. The map fires `point_clientcommand -> ov_join <mode>`.
++3. Client C++ calls `/v1/travel/request` with SteamID + mode.
++4. Backend reserves a short-lived join token and returns `{ connect, joinToken }`.
++5. Client stores the token in userinfo and runs `connect host:port`.
++6. Destination server reads the token after player activation.
++7. Server C++ validates the token with `/v1/travel/validate`.
```
--Or with a C++ HTTP client built into the GameDLL:
++## GameDLL Feature Hooks
--```text
--ov_join prophunt → ConCommand calls /v1/travel/request
-- → receives { connect, joinToken }
-- → engine.ClientCommand("connect {connect}")
-- → token stored for validation on arrival
--```
++Tracked C++ patch sources live under `sdk/openvibe/` and are copied into the local Valve SDK checkout by `tools/apply-openvibe-sdk.sh`.
++
++| Feature | Command / hook | Notes |
++| --- | --- | --- |
++| Authenticated travel | `ov_join <mode>` | Client HTTP travel request + connect. |
++| Arrival validation | `ClientActive` hook | Server validates `ov_join_token`. |
++| Main menu | `openvibe_menu`, `ov_menu` | VGUI hub selector. |
++| Steam auth | `ov_auth_steam` | Client obtains Steam Web API ticket; backend validates it. |
++| Prop Hunt disguise | `ov_prophunt_disguise`, `ov_prophunt_reset_disguise` | Server-side allowlisted model swap. |
++| Fort Wars placement | `ov_fortwars_spawn` | Server-side allowlisted physics prop spawn. |
ddiff --git a/docs/development.md b/docs/development.md
iindex 0737c2e..266a2ef 100644
---- a/docs/development.md
++++ b/docs/development.md
@@@ -89,7 +89,20 @@ Z:\home\workstation\src\openvibe-source\game\openvibe.games\maps
## Source Binaries
--The first OpenVibe binary staging uses the SDK HL2MP Linux64 output and compatibility links for TF2-branch dedicated server module names.
++OpenVibe keeps its custom Source SDK patch sources in `sdk/openvibe/` so the large Valve SDK checkout can stay local and ignored. Apply and build the patch with:
++
++```bash
++cd ~/src/openvibe-source
++tools/build-sdk-linux.sh
++```
++
++That script:
++
++1. Copies the tracked OpenVibe C++ files into `engine/source-sdk-2013/src/game/...`.
++2. Patches the HL2MP client/server VPC files.
++3. Builds the Linux64 client/server game DLLs through the Source SDK Steam Runtime container.
++
++Stage the compiled DLLs into the OpenVibe mod folder:
Run:
@@@ -114,11 +127,11 @@ game/openvibe.games/bin/linux64/client_srv.so
game/openvibe.games/bin/linux64/server_srv.so
```
--This is a temporary base until custom OpenVibe C++ code is added.
++The `_srv` compatibility links are required by the TF2-branch Linux64 SRCDS runtime.
## SRCDS
--The Source SDK Base 2013 Dedicated Server AppID `244310` installed, but its Linux depot only provided a 32-bit `srcds_linux` launcher in this environment. The current SDK build outputs Linux64 game DLLs, so OpenVibe uses Team Fortress 2 Dedicated Server AppID `232250`, which provides `srcds_linux64`.
++The Source SDK Base 2013 Dedicated Server install at `/mnt/6tb/ssd_offload/home/workstation/.steam/debian-installation/steamapps/common/Source SDK Base 2013 Dedicated Server` contains Windows server binaries in this environment. The current SDK build outputs Linux64 game DLLs, so OpenVibe uses Team Fortress 2 Dedicated Server AppID `232250`, which provides `srcds_linux64`.
Installed location:
@@@ -148,3 +161,17 @@ tools/run-deathrun.sh
tools/run-fortwars.sh
tools/run-traitortown.sh
```
++
++Full local stack:
++
++```bash
++cd ~/src/openvibe-source
++OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh
++```
++
++Stop everything:
++
++```bash
++cd ~/src/openvibe-source
++tools/dev-down.sh
++```
ddiff --git a/game/openvibe.games/maps/dr_openvibe_dev.bsp b/game/openvibe.games/maps/dr_openvibe_dev.bsp
iindex 488e2b7..a711083 100644
BBinary files a/game/openvibe.games/maps/dr_openvibe_dev.bsp and b/game/openvibe.games/maps/dr_openvibe_dev.bsp differ
ddiff --git a/game/openvibe.games/maps/fw_openvibe_dev.bsp b/game/openvibe.games/maps/fw_openvibe_dev.bsp
iindex 138625e..e0a43aa 100644
BBinary files a/game/openvibe.games/maps/fw_openvibe_dev.bsp and b/game/openvibe.games/maps/fw_openvibe_dev.bsp differ
ddiff --git a/game/openvibe.games/maps/ov_hub.bsp b/game/openvibe.games/maps/ov_hub.bsp
iindex cdc88f0..d6dea0d 100644
BBinary files a/game/openvibe.games/maps/ov_hub.bsp and b/game/openvibe.games/maps/ov_hub.bsp differ
ddiff --git a/game/openvibe.games/maps/ph_openvibe_dev.bsp b/game/openvibe.games/maps/ph_openvibe_dev.bsp
iindex 93a6343..2c08952 100644
BBinary files a/game/openvibe.games/maps/ph_openvibe_dev.bsp and b/game/openvibe.games/maps/ph_openvibe_dev.bsp differ
ddiff --git a/game/openvibe.games/maps/tt_openvibe_dev.bsp b/game/openvibe.games/maps/tt_openvibe_dev.bsp
iindex 7b7a61d..5c22909 100644
BBinary files a/game/openvibe.games/maps/tt_openvibe_dev.bsp and b/game/openvibe.games/maps/tt_openvibe_dev.bsp differ
ddiff --git a/game/openvibe.games/modelsounds.cache b/game/openvibe.games/modelsounds.cache
iindex 6bd2c93..2275a8d 100644
BBinary files a/game/openvibe.games/modelsounds.cache and b/game/openvibe.games/modelsounds.cache differ
ddiff --git a/game/openvibe.games/resource/GameMenu.res b/game/openvibe.games/resource/GameMenu.res
nnew file mode 100644
iindex 0000000..0a34088
---- /dev/null
++++ b/game/openvibe.games/resource/GameMenu.res
@@@ -0,0 +1,57 @@
++"GameMenu"
++{
++ "1"
++ {
++ "label" "OpenVibe Hub"
++ "command" "engine openvibe_menu"
++ "OnlyInGame" "0"
++ }
++ "2"
++ {
++ "label" "Enter Hub"
++ "command" "engine ov_join hub"
++ "OnlyInGame" "0"
++ }
++ "3"
++ {
++ "label" "Prop Hunt"
++ "command" "engine ov_join prophunt"
++ "OnlyInGame" "0"
++ }
++ "4"
++ {
++ "label" "Deathrun"
++ "command" "engine ov_join deathrun"
++ "OnlyInGame" "0"
++ }
++ "5"
++ {
++ "label" "Fort Wars"
++ "command" "engine ov_join fortwars"
++ "OnlyInGame" "0"
++ }
++ "6"
++ {
++ "label" "Traitor Town"
++ "command" "engine ov_join traitortown"
++ "OnlyInGame" "0"
++ }
++ "7"
++ {
++ "label" ""
++ "command" ""
++ "OnlyInGame" "0"
++ }
++ "8"
++ {
++ "label" "Options"
++ "command" "OpenOptionsDialog"
++ "OnlyInGame" "0"
++ }
++ "9"
++ {
++ "label" "Quit"
++ "command" "Quit"
++ "OnlyInGame" "0"
++ }
++}
ddiff --git a/game/openvibe.games/scripts/soundscapes_manifest.txt b/game/openvibe.games/scripts/soundscapes_manifest.txt
iindex 203eeb5..43bb3e4 100644
---- a/game/openvibe.games/scripts/soundscapes_manifest.txt
++++ b/game/openvibe.games/scripts/soundscapes_manifest.txt
@@@ -1,4 +1,4 @@
--soundscapes_manifest
++soundscaples_manifest
{
"file" "scripts/soundscapes_openvibe.txt"
}
ddiff --git a/game/openvibe.games/scripts/soundscapes_openvibe.txt b/game/openvibe.games/scripts/soundscapes_openvibe.txt
iindex 244c27a..5d4eb70 100644
---- a/game/openvibe.games/scripts/soundscapes_openvibe.txt
++++ b/game/openvibe.games/scripts/soundscapes_openvibe.txt
@@@ -1 +1,4 @@
--// OpenVibe: Source placeholder soundscape file.
++"openvibe.blank"
++{
++ "dsp" "1"
++}
ddiff --git a/game/openvibe.games/scripts/vscripts/ov_shared.nut b/game/openvibe.games/scripts/vscripts/ov_shared.nut
iindex e949c69..bf54319 100644
---- a/game/openvibe.games/scripts/vscripts/ov_shared.nut
++++ b/game/openvibe.games/scripts/vscripts/ov_shared.nut
@@@ -205,7 +205,7 @@ function OV_CreateThinkEnt(thinkFuncName) {
printl("[OV] ERROR: could not create think entity for " + thinkFuncName)
return null
}
-- ent.SetName("ov_think_" + thinkFuncName)
++ ent.__KeyValueFromString("targetname", "ov_think_" + thinkFuncName)
AddThinkToEnt(ent, thinkFuncName)
printl("[OV] think entity created for " + thinkFuncName)
return ent
ddiff --git a/hammer/vmf/dr_openvibe_dev.vmf b/hammer/vmf/dr_openvibe_dev.vmf
iindex 3b24ba9..2fb0118 100644
---- a/hammer/vmf/dr_openvibe_dev.vmf
++++ b/hammer/vmf/dr_openvibe_dev.vmf
@@@ -865,7 +865,7 @@ world
"wait" "1"
connections
{
-- "OnStartTouch" "cmd_return_hub,Command,connect 127.0.0.1:27015,0,-1"
++ "OnStartTouch" "cmd_return_hub,Command,ov_join hub,0,-1"
}
solid
{
ddiff --git a/hammer/vmf/fw_openvibe_dev.vmf b/hammer/vmf/fw_openvibe_dev.vmf
iindex fa53b87..be27285 100644
---- a/hammer/vmf/fw_openvibe_dev.vmf
++++ b/hammer/vmf/fw_openvibe_dev.vmf
@@@ -865,7 +865,7 @@ world
"wait" "1"
connections
{
-- "OnStartTouch" "cmd_return_hub,Command,connect 127.0.0.1:27015,0,-1"
++ "OnStartTouch" "cmd_return_hub,Command,ov_join hub,0,-1"
}
solid
{
ddiff --git a/hammer/vmf/ov_hub.vmf b/hammer/vmf/ov_hub.vmf
iindex 0893434..38aa556 100644
---- a/hammer/vmf/ov_hub.vmf
++++ b/hammer/vmf/ov_hub.vmf
@@@ -1080,7 +1080,7 @@ world
"wait" "1"
connections
{
-- "OnStartTouch" "cmd_join_prophunt,Command,connect 127.0.0.1:27016,0,-1"
++ "OnStartTouch" "cmd_join_prophunt,Command,ov_join prophunt,0,-1"
}
solid
{
@@@ -1178,7 +1178,7 @@ world
"wait" "1"
connections
{
-- "OnStartTouch" "cmd_join_deathrun,Command,connect 127.0.0.1:27017,0,-1"
++ "OnStartTouch" "cmd_join_deathrun,Command,ov_join deathrun,0,-1"
}
solid
{
@@@ -1276,7 +1276,7 @@ world
"wait" "1"
connections
{
-- "OnStartTouch" "cmd_join_fortwars,Command,connect 127.0.0.1:27018,0,-1"
++ "OnStartTouch" "cmd_join_fortwars,Command,ov_join fortwars,0,-1"
}
solid
{
@@@ -1374,7 +1374,7 @@ world
"wait" "1"
connections
{
-- "OnStartTouch" "cmd_join_traitortown,Command,connect 127.0.0.1:27019,0,-1"
++ "OnStartTouch" "cmd_join_traitortown,Command,ov_join traitortown,0,-1"
}
solid
{
ddiff --git a/hammer/vmf/ph_openvibe_dev.vmf b/hammer/vmf/ph_openvibe_dev.vmf
iindex 83716c9..b5480db 100644
---- a/hammer/vmf/ph_openvibe_dev.vmf
++++ b/hammer/vmf/ph_openvibe_dev.vmf
@@@ -865,7 +865,7 @@ world
"wait" "1"
connections
{
-- "OnStartTouch" "cmd_return_hub,Command,connect 127.0.0.1:27015,0,-1"
++ "OnStartTouch" "cmd_return_hub,Command,ov_join hub,0,-1"
}
solid
{
ddiff --git a/hammer/vmf/tt_openvibe_dev.vmf b/hammer/vmf/tt_openvibe_dev.vmf
iindex 6210f4b..852d80a 100644
---- a/hammer/vmf/tt_openvibe_dev.vmf
++++ b/hammer/vmf/tt_openvibe_dev.vmf
@@@ -865,7 +865,7 @@ world
"wait" "1"
connections
{
-- "OnStartTouch" "cmd_return_hub,Command,connect 127.0.0.1:27015,0,-1"
++ "OnStartTouch" "cmd_return_hub,Command,ov_join hub,0,-1"
}
solid
{
ddiff --git a/infra/cdn/nginx.conf b/infra/cdn/nginx.conf
nnew file mode 100644
iindex 0000000..60b1d8c
---- /dev/null
++++ b/infra/cdn/nginx.conf
@@@ -0,0 +1,32 @@
++server {
++ listen 80;
++ server_name openvibe.games www.openvibe.games;
++
++ client_max_body_size 50m;
++
++ location /health {
++ return 200 "ok\n";
++ add_header content-type text/plain;
++ }
++
++ location /api/ {
++ proxy_pass http://api:3000/;
++ proxy_http_version 1.1;
++ proxy_set_header host $host;
++ proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for;
++ proxy_set_header x-forwarded-proto $scheme;
++ }
++
++ location /cdn/ {
++ alias /var/www/openvibe-cdn/;
++ autoindex off;
++ access_log off;
++ expires 30d;
++ add_header cache-control "public, immutable";
++ add_header x-openvibe-cdn "openvibe.games";
++ }
++
++ location / {
++ return 302 /cdn/;
++ }
++}
ddiff --git a/infra/docker-compose.yml b/infra/docker-compose.yml
iindex e916b6a..c574b8e 100644
---- a/infra/docker-compose.yml
++++ b/infra/docker-compose.yml
@@@ -23,7 +23,13 @@ services:
restart: unless-stopped
environment:
DATABASE_URL: postgres://openvibe:openvibe@postgres:5432/openvibe
++ SESSION_REDIS_URL: redis://redis:6379
OPENVIBE_DEV_AUTH_ENABLED: "true"
++ OPENVIBE_PUBLIC_URL: "https://openvibe.games"
++ OPENVIBE_CDN_BASE_URL: "https://openvibe.games/cdn"
++ OPENVIBE_DEFAULT_SERVER_SECRET: "dev-secret"
++ STEAM_APP_ID: "243750"
++ STEAM_WEB_API_KEY: "${STEAM_WEB_API_KEY:-}"
HOST: "0.0.0.0"
PORT: "3000"
ports:
@@@ -31,11 +37,32 @@ services:
depends_on:
postgres:
condition: service_healthy
++ redis:
++ condition: service_started
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:3000/health || exit 1"]
interval: 10s
timeout: 5s
retries: 6
++ redis:
++ image: redis:7
++ restart: unless-stopped
++ command: ["redis-server", "--appendonly", "yes"]
++ ports:
++ - "6379:6379"
++ volumes:
++ - redis_data:/data
++
++ cdn:
++ image: nginx:1.27-alpine
++ restart: unless-stopped
++ ports:
++ - "8080:80"
++ volumes:
++ - ./cdn/nginx.conf:/etc/nginx/conf.d/default.conf:ro
++ - ../game/openvibe.games:/var/www/openvibe-cdn:ro
++
volumes:
pg_data:
++ redis_data:
ddiff --git a/infra/kubernetes/openvibe.yaml b/infra/kubernetes/openvibe.yaml
nnew file mode 100644
iindex 0000000..9643be5
---- /dev/null
++++ b/infra/kubernetes/openvibe.yaml
@@@ -0,0 +1,339 @@
++apiVersion: v1
++kind: Namespace
++metadata:
++ name: openvibe
++---
++apiVersion: v1
++kind: Secret
++metadata:
++ name: openvibe-secrets
++ namespace: openvibe
++type: Opaque
++stringData:
++ postgres-password: change-me
++ database-url: postgres://openvibe:change-me@postgres:5432/openvibe
++ admin-secret: change-me
++ server-secret: change-me
++ steam-web-api-key: change-me
++---
++apiVersion: v1
++kind: ConfigMap
++metadata:
++ name: openvibe-config
++ namespace: openvibe
++data:
++ STEAM_APP_ID: "243750"
++ OPENVIBE_PUBLIC_URL: "https://openvibe.games"
++ OPENVIBE_CDN_BASE_URL: "https://openvibe.games/cdn"
++ STEAM_WEB_API_BASE: "https://api.steampowered.com"
++---
++apiVersion: v1
++kind: ConfigMap
++metadata:
++ name: openvibe-nginx
++ namespace: openvibe
++data:
++ default.conf: |
++ server {
++ listen 80;
++ server_name openvibe.games www.openvibe.games;
++
++ client_max_body_size 50m;
++
++ location /health {
++ return 200 "ok\n";
++ add_header content-type text/plain;
++ }
++
++ location /api/ {
++ proxy_pass http://api:3000/;
++ proxy_http_version 1.1;
++ proxy_set_header host $host;
++ proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for;
++ proxy_set_header x-forwarded-proto $scheme;
++ }
++
++ location /cdn/ {
++ alias /var/www/openvibe-cdn/;
++ autoindex off;
++ access_log off;
++ expires 30d;
++ add_header cache-control "public, immutable";
++ add_header x-openvibe-cdn "openvibe.games";
++ }
++
++ location / {
++ return 302 /cdn/;
++ }
++ }
++---
++apiVersion: v1
++kind: PersistentVolumeClaim
++metadata:
++ name: postgres-data
++ namespace: openvibe
++spec:
++ accessModes: ["ReadWriteOnce"]
++ resources:
++ requests:
++ storage: 20Gi
++---
++apiVersion: v1
++kind: PersistentVolumeClaim
++metadata:
++ name: redis-data
++ namespace: openvibe
++spec:
++ accessModes: ["ReadWriteOnce"]
++ resources:
++ requests:
++ storage: 5Gi
++---
++apiVersion: v1
++kind: PersistentVolumeClaim
++metadata:
++ name: cdn-assets
++ namespace: openvibe
++spec:
++ accessModes: ["ReadWriteMany"]
++ resources:
++ requests:
++ storage: 20Gi
++---
++apiVersion: apps/v1
++kind: Deployment
++metadata:
++ name: postgres
++ namespace: openvibe
++spec:
++ replicas: 1
++ selector:
++ matchLabels:
++ app: postgres
++ template:
++ metadata:
++ labels:
++ app: postgres
++ spec:
++ containers:
++ - name: postgres
++ image: postgres:16
++ env:
++ - name: POSTGRES_USER
++ value: openvibe
++ - name: POSTGRES_DB
++ value: openvibe
++ - name: POSTGRES_PASSWORD
++ valueFrom:
++ secretKeyRef:
++ name: openvibe-secrets
++ key: postgres-password
++ ports:
++ - containerPort: 5432
++ volumeMounts:
++ - name: data
++ mountPath: /var/lib/postgresql/data
++ volumes:
++ - name: data
++ persistentVolumeClaim:
++ claimName: postgres-data
++---
++apiVersion: v1
++kind: Service
++metadata:
++ name: postgres
++ namespace: openvibe
++spec:
++ selector:
++ app: postgres
++ ports:
++ - port: 5432
++ targetPort: 5432
++---
++apiVersion: apps/v1
++kind: Deployment
++metadata:
++ name: redis
++ namespace: openvibe
++spec:
++ replicas: 1
++ selector:
++ matchLabels:
++ app: redis
++ template:
++ metadata:
++ labels:
++ app: redis
++ spec:
++ containers:
++ - name: redis
++ image: redis:7
++ args: ["redis-server", "--appendonly", "yes"]
++ ports:
++ - containerPort: 6379
++ volumeMounts:
++ - name: data
++ mountPath: /data
++ volumes:
++ - name: data
++ persistentVolumeClaim:
++ claimName: redis-data
++---
++apiVersion: v1
++kind: Service
++metadata:
++ name: redis
++ namespace: openvibe
++spec:
++ selector:
++ app: redis
++ ports:
++ - port: 6379
++ targetPort: 6379
++---
++apiVersion: apps/v1
++kind: Deployment
++metadata:
++ name: api
++ namespace: openvibe
++spec:
++ replicas: 2
++ selector:
++ matchLabels:
++ app: api
++ template:
++ metadata:
++ labels:
++ app: api
++ spec:
++ containers:
++ - name: api
++ image: ghcr.io/openvibers/openvibe.games-api:latest
++ env:
++ - name: HOST
++ value: "0.0.0.0"
++ - name: PORT
++ value: "3000"
++ - name: OPENVIBE_DEV_AUTH_ENABLED
++ value: "false"
++ - name: DATABASE_URL
++ valueFrom:
++ secretKeyRef:
++ name: openvibe-secrets
++ key: database-url
++ - name: SESSION_REDIS_URL
++ value: redis://redis:6379
++ - name: OPENVIBE_ADMIN_SECRET
++ valueFrom:
++ secretKeyRef:
++ name: openvibe-secrets
++ key: admin-secret
++ - name: OPENVIBE_DEFAULT_SERVER_SECRET
++ valueFrom:
++ secretKeyRef:
++ name: openvibe-secrets
++ key: server-secret
++ - name: STEAM_WEB_API_KEY
++ valueFrom:
++ secretKeyRef:
++ name: openvibe-secrets
++ key: steam-web-api-key
++ envFrom:
++ - configMapRef:
++ name: openvibe-config
++ ports:
++ - containerPort: 3000
++ readinessProbe:
++ httpGet:
++ path: /health
++ port: 3000
++ initialDelaySeconds: 5
++ periodSeconds: 10
++---
++apiVersion: v1
++kind: Service
++metadata:
++ name: api
++ namespace: openvibe
++spec:
++ selector:
++ app: api
++ ports:
++ - port: 3000
++ targetPort: 3000
++---
++apiVersion: apps/v1
++kind: Deployment
++metadata:
++ name: cdn
++ namespace: openvibe
++spec:
++ replicas: 2
++ selector:
++ matchLabels:
++ app: cdn
++ template:
++ metadata:
++ labels:
++ app: cdn
++ spec:
++ containers:
++ - name: nginx
++ image: nginx:1.27-alpine
++ ports:
++ - containerPort: 80
++ volumeMounts:
++ - name: assets
++ mountPath: /var/www/openvibe-cdn
++ readOnly: true
++ - name: nginx-config
++ mountPath: /etc/nginx/conf.d/default.conf
++ subPath: default.conf
++ readOnly: true
++ volumes:
++ - name: assets
++ persistentVolumeClaim:
++ claimName: cdn-assets
++ - name: nginx-config
++ configMap:
++ name: openvibe-nginx
++---
++apiVersion: v1
++kind: Service
++metadata:
++ name: cdn
++ namespace: openvibe
++spec:
++ selector:
++ app: cdn
++ ports:
++ - port: 80
++ targetPort: 80
++---
++apiVersion: networking.k8s.io/v1
++kind: Ingress
++metadata:
++ name: openvibe-web
++ namespace: openvibe
++spec:
++ rules:
++ - host: openvibe.games
++ http:
++ paths:
++ - path: /
++ pathType: Prefix
++ backend:
++ service:
++ name: cdn
++ port:
++ number: 80
++ - host: www.openvibe.games
++ http:
++ paths:
++ - path: /
++ pathType: Prefix
++ backend:
++ service:
++ name: cdn
++ port:
++ number: 80
ddiff --git a/infra/production/docker-stack.yml b/infra/production/docker-stack.yml
nnew file mode 100644
iindex 0000000..77192b1
---- /dev/null
++++ b/infra/production/docker-stack.yml
@@@ -0,0 +1,79 @@
++version: "3.9"
++
++services:
++ api:
++ image: ghcr.io/openvibers/openvibe.games-api:latest
++ environment:
++ HOST: "0.0.0.0"
++ PORT: "3000"
++ DATABASE_URL: "postgres://openvibe:${POSTGRES_PASSWORD}@postgres:5432/openvibe"
++ SESSION_REDIS_URL: "redis://redis:6379"
++ OPENVIBE_PUBLIC_URL: "https://openvibe.games"
++ OPENVIBE_CDN_BASE_URL: "https://openvibe.games/cdn"
++ OPENVIBE_DEV_AUTH_ENABLED: "false"
++ OPENVIBE_ADMIN_SECRET: "${OPENVIBE_ADMIN_SECRET}"
++ OPENVIBE_DEFAULT_SERVER_SECRET: "${OPENVIBE_DEFAULT_SERVER_SECRET}"
++ STEAM_APP_ID: "${STEAM_APP_ID}"
++ STEAM_WEB_API_KEY: "${STEAM_WEB_API_KEY}"
++ STEAM_WEB_API_BASE: "https://api.steampowered.com"
++ networks:
++ - openvibe
++ deploy:
++ replicas: 2
++ update_config:
++ order: start-first
++ restart_policy:
++ condition: on-failure
++
++ postgres:
++ image: postgres:16
++ environment:
++ POSTGRES_USER: openvibe
++ POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
++ POSTGRES_DB: openvibe
++ volumes:
++ - pg_data:/var/lib/postgresql/data
++ networks:
++ - openvibe
++ deploy:
++ replicas: 1
++ placement:
++ constraints:
++ - node.role == manager
++
++ redis:
++ image: redis:7
++ command: ["redis-server", "--appendonly", "yes"]
++ volumes:
++ - redis_data:/data
++ networks:
++ - openvibe
++ deploy:
++ replicas: 1
++
++ cdn:
++ image: nginx:1.27-alpine
++ ports:
++ - "80:80"
++ configs:
++ - source: openvibe_nginx
++ target: /etc/nginx/conf.d/default.conf
++ volumes:
++ - cdn_assets:/var/www/openvibe-cdn:ro
++ networks:
++ - openvibe
++ deploy:
++ replicas: 2
++
++networks:
++ openvibe:
++ driver: overlay
++
++volumes:
++ pg_data:
++ redis_data:
++ cdn_assets:
++
++configs:
++ openvibe_nginx:
++ file: ../cdn/nginx.conf
ddiff --git a/launcher/main.js b/launcher/main.js
iindex 153d45e..7cccbee 100644
---- a/launcher/main.js
++++ b/launcher/main.js
@@@ -126,8 +126,8 @@ ipcMain.handle('api:leaderboard', async (_e, limit = 10) => {
catch { return []; }
});
--ipcMain.handle('api:travel', async (_e, { steamId, targetMode }) => {
-- try { return await apiPost('/v1/travel/request', { steamId, targetMode }); }
++ipcMain.handle('api:travel', async (_e, { steamId, mode }) => {
++ try { return await apiPost('/v1/travel/request', { steamId, mode }); }
catch (e) { return { error: e.message }; }
});
ddiff --git a/sdk/openvibe/client/hl2mp/openvibe_client.cpp b/sdk/openvibe/client/hl2mp/openvibe_client.cpp
nnew file mode 100644
iindex 0000000..f55b4dc
---- /dev/null
++++ b/sdk/openvibe/client/hl2mp/openvibe_client.cpp
@@@ -0,0 +1,452 @@
++// OpenVibe: Source client integration.
++//
++// This file belongs to the HL2MP client DLL. It provides:
++// - ov_join <mode>: ask the OpenVibe API for a travel reservation, store the
++// join token in userinfo, then connect to the selected Source server.
++// - ov_auth_steam: request a Steam Web API auth ticket and send it to the
++// OpenVibe API for production authentication.
++// - ov_open_url: small convenience command for opening openvibe.games.
++
++#include "cbase.h"
++#include "cdll_client_int.h"
++#include "ienginevgui.h"
++
++#include "steam/steam_api.h"
++#include "steam/isteamfriends.h"
++#include "steam/isteamhttp.h"
++#include "steam/isteamuser.h"
++
++// memdbgon must be the last include file in a .cpp file.
++#include "tier0/memdbgon.h"
++
++extern CSteamAPIContext *steamapicontext;
++
++static ConVar ov_api_url(
++ "ov_api_url",
++ "http://127.0.0.1:3000",
++ FCVAR_ARCHIVE,
++ "OpenVibe API base URL." );
++
++static ConVar ov_dev_steamid(
++ "ov_dev_steamid",
++ "76561198000000000",
++ FCVAR_ARCHIVE,
++ "Fallback SteamID used when Steam is unavailable in local development." );
++
++static ConVar ov_join_local_fallback(
++ "ov_join_local_fallback",
++ "1",
++ FCVAR_ARCHIVE,
++ "If API travel fails, connect to the conventional local OpenVibe port for the requested mode." );
++
++static ConVar ov_join_token(
++ "ov_join_token",
++ "",
++ FCVAR_USERINFO | FCVAR_ARCHIVE,
++ "Short-lived OpenVibe join token sent to destination servers." );
++
++static ConVar ov_auth_identity(
++ "ov_auth_identity",
++ "openvibe.games",
++ FCVAR_ARCHIVE,
++ "Steam Web API ticket identity for OpenVibe backend authentication." );
++
++static const char *s_OpenVibeModes[] =
++{
++ "hub",
++ "prophunt",
++ "deathrun",
++ "fortwars",
++ "traitortown",
++};
++
++static bool OV_IsModeAllowed( const char *pszMode )
++{
++ for ( int i = 0; i < ARRAYSIZE( s_OpenVibeModes ); ++i )
++ {
++ if ( !Q_stricmp( pszMode, s_OpenVibeModes[i] ) )
++ return true;
++ }
++ return false;
++}
++
++static const char *OV_LocalConnectForMode( const char *pszMode )
++{
++ if ( !Q_stricmp( pszMode, "hub" ) ) return "127.0.0.1:27015";
++ if ( !Q_stricmp( pszMode, "prophunt" ) ) return "127.0.0.1:27016";
++ if ( !Q_stricmp( pszMode, "deathrun" ) ) return "127.0.0.1:27017";
++ if ( !Q_stricmp( pszMode, "fortwars" ) ) return "127.0.0.1:27018";
++ if ( !Q_stricmp( pszMode, "traitortown" ) ) return "127.0.0.1:27019";
++ return "127.0.0.1:27015";
++}
++
++static void OV_GetSteamIdString( char *pszOut, size_t nOut )
++{
++ if ( steamapicontext && steamapicontext->SteamUser() && steamapicontext->SteamUser()->BLoggedOn() )
++ {
++ Q_snprintf( pszOut, nOut, "%llu", steamapicontext->SteamUser()->GetSteamID().ConvertToUint64() );
++ return;
++ }
++
++ Q_strncpy( pszOut, ov_dev_steamid.GetString(), nOut );
++}
++
++static bool OV_ExtractJsonString( const char *pszJson, const char *pszKey, char *pszOut, size_t nOut )
++{
++ char szNeedle[128];
++ Q_snprintf( szNeedle, sizeof( szNeedle ), "\"%s\":\"", pszKey );
++
++ const char *pszStart = Q_strstr( pszJson, szNeedle );
++ if ( !pszStart )
++ return false;
++
++ pszStart += Q_strlen( szNeedle );
++ const char *pszEnd = Q_strstr( pszStart, "\"" );
++ if ( !pszEnd )
++ return false;
++
++ const int nLen = pszEnd - pszStart;
++ if ( nLen <= 0 || nLen >= (int)nOut )
++ return false;
++
++ Q_strncpy( pszOut, pszStart, nLen + 1 );
++ pszOut[nLen] = '\0';
++ return true;
++}
++
++static bool OV_ExtractJsonBool( const char *pszJson, const char *pszKey )
++{
++ char szNeedle[128];
++ Q_snprintf( szNeedle, sizeof( szNeedle ), "\"%s\":true", pszKey );
++ return Q_strstr( pszJson, szNeedle ) != NULL;
++}
++
++static void OV_ConnectWithReservation( const char *pszConnect, const char *pszToken )
++{
++ char szCommand[1024];
++ Q_snprintf(
++ szCommand,
++ sizeof( szCommand ),
++ "setinfo ov_join_token \"%s\"\nconnect %s\n",
++ pszToken,
++ pszConnect );
++
++ Msg( "[OV] Connecting to %s with join token.\n", pszConnect );
++ engine->ClientCmd_Unrestricted( szCommand );
++}
++
++class COpenVibeTravelClient
++{
++public:
++ COpenVibeTravelClient() : m_hRequest( INVALID_HTTPREQUEST_HANDLE )
++ {
++ m_szMode[0] = '\0';
++ }
++
++ void RequestTravel( const char *pszMode )
++ {
++ if ( !OV_IsModeAllowed( pszMode ) )
++ {
++ Warning( "[OV] Unknown mode '%s'. Use hub, prophunt, deathrun, fortwars, or traitortown.\n", pszMode );
++ return;
++ }
++
++ Q_strncpy( m_szMode, pszMode, sizeof( m_szMode ) );
++
++ if ( !steamapicontext || !steamapicontext->SteamHTTP() )
++ {
++ Warning( "[OV] Steam HTTP unavailable; using local fallback for %s.\n", pszMode );
++ LocalFallback();
++ return;
++ }
++
++ char szSteamId[32];
++ OV_GetSteamIdString( szSteamId, sizeof( szSteamId ) );
++
++ char szUrl[512];
++ Q_snprintf( szUrl, sizeof( szUrl ), "%s/v1/travel/request", ov_api_url.GetString() );
++
++ char szBody[256];
++ Q_snprintf( szBody, sizeof( szBody ), "{\"steamId\":\"%s\",\"mode\":\"%s\"}", szSteamId, pszMode );
++
++ ReleaseRequest();
++ m_hRequest = steamapicontext->SteamHTTP()->CreateHTTPRequest( k_EHTTPMethodPOST, szUrl );
++ if ( m_hRequest == INVALID_HTTPREQUEST_HANDLE )
++ {
++ Warning( "[OV] Could not create travel HTTP request.\n" );
++ LocalFallback();
++ return;
++ }
++
++ steamapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hRequest, "Accept", "application/json" );
++ steamapicontext->SteamHTTP()->SetHTTPRequestRawPostBody(
++ m_hRequest,
++ "application/json",
++ (uint8 *)szBody,
++ Q_strlen( szBody ) );
++ steamapicontext->SteamHTTP()->SetHTTPRequestNetworkActivityTimeout( m_hRequest, 8 );
++ steamapicontext->SteamHTTP()->SetHTTPRequestAbsoluteTimeoutMS( m_hRequest, 10000 );
++
++ SteamAPICall_t hCall = k_uAPICallInvalid;
++ if ( !steamapicontext->SteamHTTP()->SendHTTPRequest( m_hRequest, &hCall ) || hCall == k_uAPICallInvalid )
++ {
++ Warning( "[OV] Could not send travel HTTP request.\n" );
++ ReleaseRequest();
++ LocalFallback();
++ return;
++ }
++
++ m_Callback.Set( hCall, this, &COpenVibeTravelClient::OnTravelResponse );
++ Msg( "[OV] Travel request sent for mode '%s'.\n", pszMode );
++ }
++
++private:
++ void OnTravelResponse( HTTPRequestCompleted_t *pResult, bool bIOFailure )
++ {
++ if ( !pResult || bIOFailure || !pResult->m_bRequestSuccessful ||
++ pResult->m_eStatusCode < k_EHTTPStatusCode200OK ||
++ pResult->m_eStatusCode >= k_EHTTPStatusCode300MultipleChoices )
++ {
++ Warning( "[OV] Travel request failed.\n" );
++ ReleaseRequest();
++ LocalFallback();
++ return;
++ }
++
++ char szBody[4096];
++ szBody[0] = '\0';
++
++ uint32 nBodySize = 0;
++ if ( steamapicontext->SteamHTTP()->GetHTTPResponseBodySize( pResult->m_hRequest, &nBodySize ) &&
++ nBodySize > 0 &&
++ nBodySize < sizeof( szBody ) )
++ {
++ if ( steamapicontext->SteamHTTP()->GetHTTPResponseBodyData(
++ pResult->m_hRequest,
++ (uint8 *)szBody,
++ nBodySize ) )
++ {
++ szBody[nBodySize] = '\0';
++ }
++ }
++
++ char szConnect[256];
++ char szToken[256];
++ if ( !OV_ExtractJsonString( szBody, "connect", szConnect, sizeof( szConnect ) ) ||
++ !OV_ExtractJsonString( szBody, "joinToken", szToken, sizeof( szToken ) ) )
++ {
++ Warning( "[OV] Travel response did not contain connect/joinToken: %s\n", szBody );
++ ReleaseRequest();
++ LocalFallback();
++ return;
++ }
++
++ ReleaseRequest();
++ OV_ConnectWithReservation( szConnect, szToken );
++ }
++
++ void LocalFallback()
++ {
++ if ( !ov_join_local_fallback.GetBool() )
++ return;
++
++ char szToken[128];
++ Q_snprintf( szToken, sizeof( szToken ), "local-dev-%s", m_szMode[0] ? m_szMode : "hub" );
++ OV_ConnectWithReservation( OV_LocalConnectForMode( m_szMode ), szToken );
++ }
++
++ void ReleaseRequest()
++ {
++ if ( m_hRequest != INVALID_HTTPREQUEST_HANDLE && steamapicontext && steamapicontext->SteamHTTP() )
++ {
++ steamapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hRequest );
++ }
++ m_hRequest = INVALID_HTTPREQUEST_HANDLE;
++ }
++
++ char m_szMode[32];
++ HTTPRequestHandle m_hRequest;
++ CCallResult< COpenVibeTravelClient, HTTPRequestCompleted_t > m_Callback;
++};
++
++static COpenVibeTravelClient g_OpenVibeTravelClient;
++
++static void OV_Join_f( const CCommand &args )
++{
++ if ( args.ArgC() < 2 )
++ {
++ Msg( "Usage: ov_join <hub|prophunt|deathrun|fortwars|traitortown>\n" );
++ return;
++ }
++
++ g_OpenVibeTravelClient.RequestTravel( args[1] );
++}
++
++static ConCommand ov_join_cmd(
++ "ov_join",
++ OV_Join_f,
++ "Reserve an OpenVibe travel token and connect to a game mode server.",
++ FCVAR_CLIENTDLL );
++
++class COpenVibeSteamAuthClient
++{
++public:
++ COpenVibeSteamAuthClient() :
++ m_hRequest( INVALID_HTTPREQUEST_HANDLE ),
++ m_CallbackTicket( this, &COpenVibeSteamAuthClient::OnTicket )
++ {
++ }
++
++ void RequestAuth()
++ {
++ if ( !steamapicontext || !steamapicontext->SteamUser() )
++ {
++ Warning( "[OV Auth] Steam user interface unavailable.\n" );
++ return;
++ }
++
++ steamapicontext->SteamUser()->GetAuthTicketForWebApi( ov_auth_identity.GetString() );
++ Msg( "[OV Auth] Requested Steam Web API ticket.\n" );
++ }
++
++private:
++ void OnTicket( GetTicketForWebApiResponse_t *pTicket )
++ {
++ if ( !pTicket || pTicket->m_eResult != k_EResultOK || pTicket->m_cubTicket <= 0 )
++ {
++ Warning( "[OV Auth] Steam did not provide a valid Web API ticket.\n" );
++ return;
++ }
++
++ char szTicketHex[GetTicketForWebApiResponse_t::k_nCubTicketMaxLength * 2 + 1];
++ for ( int i = 0; i < pTicket->m_cubTicket; ++i )
++ {
++ Q_snprintf( szTicketHex + ( i * 2 ), sizeof( szTicketHex ) - ( i * 2 ), "%02x", pTicket->m_rgubTicket[i] );
++ }
++ szTicketHex[pTicket->m_cubTicket * 2] = '\0';
++
++ PostTicket( szTicketHex );
++ }
++
++ void PostTicket( const char *pszTicketHex )
++ {
++ if ( !steamapicontext || !steamapicontext->SteamHTTP() )
++ {
++ Warning( "[OV Auth] Steam HTTP unavailable.\n" );
++ return;
++ }
++
++ char szUrl[512];
++ Q_snprintf( szUrl, sizeof( szUrl ), "%s/v1/auth/steam", ov_api_url.GetString() );
++
++ char szBody[GetTicketForWebApiResponse_t::k_nCubTicketMaxLength * 2 + 256];
++ Q_snprintf(
++ szBody,
++ sizeof( szBody ),
++ "{\"ticket\":\"%s\",\"identity\":\"%s\"}",
++ pszTicketHex,
++ ov_auth_identity.GetString() );
++
++ ReleaseRequest();
++ m_hRequest = steamapicontext->SteamHTTP()->CreateHTTPRequest( k_EHTTPMethodPOST, szUrl );
++ if ( m_hRequest == INVALID_HTTPREQUEST_HANDLE )
++ return;
++
++ steamapicontext->SteamHTTP()->SetHTTPRequestHeaderValue( m_hRequest, "Accept", "application/json" );
++ steamapicontext->SteamHTTP()->SetHTTPRequestRawPostBody(
++ m_hRequest,
++ "application/json",
++ (uint8 *)szBody,
++ Q_strlen( szBody ) );
++ steamapicontext->SteamHTTP()->SetHTTPRequestNetworkActivityTimeout( m_hRequest, 8 );
++ steamapicontext->SteamHTTP()->SetHTTPRequestAbsoluteTimeoutMS( m_hRequest, 10000 );
++
++ SteamAPICall_t hCall = k_uAPICallInvalid;
++ if ( steamapicontext->SteamHTTP()->SendHTTPRequest( m_hRequest, &hCall ) && hCall != k_uAPICallInvalid )
++ {
++ m_CallbackHTTP.Set( hCall, this, &COpenVibeSteamAuthClient::OnAuthResponse );
++ }
++ else
++ {
++ ReleaseRequest();
++ }
++ }
++
++ void OnAuthResponse( HTTPRequestCompleted_t *pResult, bool bIOFailure )
++ {
++ char szBody[4096];
++ szBody[0] = '\0';
++
++ if ( !bIOFailure && pResult && pResult->m_bRequestSuccessful )
++ {
++ uint32 nBodySize = 0;
++ if ( steamapicontext->SteamHTTP()->GetHTTPResponseBodySize( pResult->m_hRequest, &nBodySize ) &&
++ nBodySize > 0 &&
++ nBodySize < sizeof( szBody ) )
++ {
++ if ( steamapicontext->SteamHTTP()->GetHTTPResponseBodyData(
++ pResult->m_hRequest,
++ (uint8 *)szBody,
++ nBodySize ) )
++ {
++ szBody[nBodySize] = '\0';
++ }
++ }
++ }
++
++ if ( OV_ExtractJsonBool( szBody, "authenticated" ) )
++ {
++ Msg( "[OV Auth] Steam authentication accepted by OpenVibe API.\n" );
++ }
++ else
++ {
++ Warning( "[OV Auth] OpenVibe API rejected or could not validate Steam ticket: %s\n", szBody );
++ }
++
++ ReleaseRequest();
++ }
++
++ void ReleaseRequest()
++ {
++ if ( m_hRequest != INVALID_HTTPREQUEST_HANDLE && steamapicontext && steamapicontext->SteamHTTP() )
++ {
++ steamapicontext->SteamHTTP()->ReleaseHTTPRequest( m_hRequest );
++ }
++ m_hRequest = INVALID_HTTPREQUEST_HANDLE;
++ }
++
++ HTTPRequestHandle m_hRequest;
++ CCallback< COpenVibeSteamAuthClient, GetTicketForWebApiResponse_t > m_CallbackTicket;
++ CCallResult< COpenVibeSteamAuthClient, HTTPRequestCompleted_t > m_CallbackHTTP;
++};
++
++static COpenVibeSteamAuthClient g_OpenVibeSteamAuthClient;
++
++static void OV_AuthSteam_f( const CCommand &args )
++{
++ g_OpenVibeSteamAuthClient.RequestAuth();
++}
++
++static ConCommand ov_auth_steam_cmd(
++ "ov_auth_steam",
++ OV_AuthSteam_f,
++ "Request a Steam Web API auth ticket and authenticate with OpenVibe.",
++ FCVAR_CLIENTDLL );
++
++static void OV_OpenURL_f( const CCommand &args )
++{
++ const char *pszUrl = args.ArgC() >= 2 ? args[1] : "https://openvibe.games";
++ if ( steamapicontext && steamapicontext->SteamFriends() )
++ {
++ steamapicontext->SteamFriends()->ActivateGameOverlayToWebPage( pszUrl );
++ return;
++ }
++
++ engine->ClientCmd_Unrestricted( VarArgs( "echo Open %s in your browser.\n", pszUrl ) );
++}
++
++static ConCommand ov_open_url_cmd(
++ "ov_open_url",
++ OV_OpenURL_f,
++ "Open the OpenVibe website in the Steam overlay.",
++ FCVAR_CLIENTDLL );
ddiff --git a/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp b/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp
nnew file mode 100644
iindex 0000000..0d51d07
---- /dev/null
++++ b/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp
@@@ -0,0 +1,143 @@
++// OpenVibe: Source VGUI portal panel.
++//
++// This is intentionally small and native. The Electron launcher is still the
++// rich desktop menu, while this panel gives the Source client an in-game
++// OpenVibe-first menu that can replace the normal server-browser workflow.
++
++#include "cbase.h"
++#include "cdll_client_int.h"
++#include "ienginevgui.h"
++
++#include <vgui/IScheme.h>
++#include <vgui/ISurface.h>
++#include <vgui_controls/Button.h>
++#include <vgui_controls/Frame.h>
++#include <vgui_controls/Label.h>
++
++// memdbgon must be the last include file in a .cpp file.
++#include "tier0/memdbgon.h"
++
++using namespace vgui;
++
++class COpenVibeMenuPanel : public Frame
++{
++ DECLARE_CLASS_SIMPLE( COpenVibeMenuPanel, Frame );
++
++public:
++ COpenVibeMenuPanel( VPANEL parent ) :
++ BaseClass( NULL, "OpenVibeMenuPanel" )
++ {
++ SetParent( parent );
++ SetTitle( "OpenVibe: Source", true );
++ SetSize( 760, 500 );
++ SetMinimumSize( 640, 420 );
++ SetSizeable( false );
++ SetMoveable( true );
++ SetDeleteSelfOnClose( false );
++ SetBgColor( Color( 18, 20, 28, 245 ) );
++
++ int sw = 0;
++ int sh = 0;
++ surface()->GetScreenSize( sw, sh );
++ SetPos( ( sw - GetWide() ) / 2, ( sh - GetTall() ) / 2 );
++
++ Label *pTitle = new Label( this, "OpenVibeTitle", "OpenVibe.Games" );
++ pTitle->SetPos( 28, 36 );
++ pTitle->SetSize( 420, 32 );
++ pTitle->SetFgColor( Color( 0, 210, 255, 255 ) );
++
++ Label *pSubtitle = new Label( this, "OpenVibeSubtitle", "Choose a hub or minigame server. Travel is routed through the OpenVibe backend." );
++ pSubtitle->SetPos( 28, 70 );
++ pSubtitle->SetSize( 680, 24 );
++ pSubtitle->SetFgColor( Color( 220, 230, 240, 255 ) );
++
++ AddModeButton( "Hub", "Social lobby, portals, NPCs, shops", "hub", 28, 120 );
++ AddModeButton( "Prop Hunt", "Props disguise and hide from hunters", "prophunt", 390, 120 );
++ AddModeButton( "Deathrun", "Runners survive traps and reach the finish", "deathrun", 28, 220 );
++ AddModeButton( "Fort Wars", "Build phase, prop forts, combat phase", "fortwars", 390, 220 );
++ AddModeButton( "Traitor Town", "Social deduction with traitors and innocents", "traitortown", 28, 320 );
++
++ Button *pWebsite = new Button( this, "OpenWebsite", "Open openvibe.games", this, "website" );
++ pWebsite->SetPos( 390, 320 );
++ pWebsite->SetSize( 320, 44 );
++
++ Button *pClose = new Button( this, "Close", "Close", this, "close" );
++ pClose->SetPos( 590, 430 );
++ pClose->SetSize( 120, 36 );
++ }
++
++ void OnCommand( const char *pszCommand ) OVERRIDE
++ {
++ if ( !Q_strncmp( pszCommand, "join:", 5 ) )
++ {
++ engine->ClientCmd_Unrestricted( VarArgs( "gameui_hide\nov_join %s\n", pszCommand + 5 ) );
++ SetVisible( false );
++ return;
++ }
++
++ if ( !Q_stricmp( pszCommand, "website" ) )
++ {
++ engine->ClientCmd_Unrestricted( "ov_open_url https://openvibe.games\n" );
++ return;
++ }
++
++ if ( !Q_stricmp( pszCommand, "close" ) )
++ {
++ SetVisible( false );
++ return;
++ }
++
++ BaseClass::OnCommand( pszCommand );
++ }
++
++private:
++ void AddModeButton( const char *pszName, const char *pszDescription, const char *pszMode, int x, int y )
++ {
++ char szButtonName[64];
++ char szCommand[64];
++ char szDescriptionName[64];
++ Q_snprintf( szButtonName, sizeof( szButtonName ), "%sButton", pszMode );
++ Q_snprintf( szCommand, sizeof( szCommand ), "join:%s", pszMode );
++ Q_snprintf( szDescriptionName, sizeof( szDescriptionName ), "%sDescription", pszMode );
++
++ Button *pButton = new Button( this, szButtonName, pszName, this, szCommand );
++ pButton->SetPos( x, y );
++ pButton->SetSize( 320, 48 );
++
++ Label *pDescription = new Label( this, szDescriptionName, pszDescription );
++ pDescription->SetPos( x + 2, y + 52 );
++ pDescription->SetSize( 320, 26 );
++ pDescription->SetFgColor( Color( 185, 195, 205, 255 ) );
++ }
++};
++
++static COpenVibeMenuPanel *s_pOpenVibeMenu = NULL;
++
++static void OV_OpenMenu()
++{
++ if ( !s_pOpenVibeMenu )
++ {
++ s_pOpenVibeMenu = new COpenVibeMenuPanel( enginevgui->GetPanel( PANEL_GAMEUIDLL ) );
++ }
++
++ s_pOpenVibeMenu->SetVisible( true );
++ s_pOpenVibeMenu->MoveToFront();
++ s_pOpenVibeMenu->RequestFocus();
++}
++
++static void OV_OpenMenu_f( const CCommand &args )
++{
++ OV_OpenMenu();
++}
++
++static ConCommand openvibe_menu_cmd(
++ "openvibe_menu",
++ OV_OpenMenu_f,
++ "Open the OpenVibe hub/minigame selection menu.",
++ FCVAR_CLIENTDLL );
++
++static ConCommand ov_menu_cmd(
++ "ov_menu",
++ OV_OpenMenu_f,
++ "Open the OpenVibe hub/minigame selection menu.",
++ FCVAR_CLIENTDLL );
ddiff --git a/sdk/openvibe/server/hl2mp/openvibe_server.cpp b/sdk/openvibe/server/hl2mp/openvibe_server.cpp
nnew file mode 100644
iindex 0000000..2ff7564
---- /dev/null
++++ b/sdk/openvibe/server/hl2mp/openvibe_server.cpp
@@@ -0,0 +1,364 @@
++// OpenVibe: Source server integration.
++//
++// This file belongs to the HL2MP server DLL. It provides:
++// - destination-server join-token validation through /v1/travel/validate
++// - ov_prophunt_disguise <prop>: whitelisted prop model disguise
++// - ov_prophunt_reset_disguise: restore normal player model
++// - ov_fortwars_spawn <prop>: whitelisted prop_physics placement
++
++#include "cbase.h"
++#include "hl2mp_player.h"
++#include "player.h"
++#include "props.h"
++#include "util.h"
++
++#include "steam/steam_gameserver.h"
++#include "steam/isteamhttp.h"
++
++// memdbgon must be the last include file in a .cpp file.
++#include "tier0/memdbgon.h"
++
++static ConVar ov_api_url(
++ "ov_api_url",
++ "http://127.0.0.1:3000",
++ FCVAR_GAMEDLL,
++ "OpenVibe API base URL." );
++
++static ConVar ov_server_id(
++ "ov_server_id",
++ "local-hub-27015",
++ FCVAR_GAMEDLL,
++ "OpenVibe server registry ID for this SRCDS instance." );
++
++static ConVar ov_join_required(
++ "ov_join_required",
++ "0",
++ FCVAR_GAMEDLL,
++ "When enabled, players without a valid OpenVibe join token are kicked." );
++
++static ConVar ov_fortwars_build_enabled(
++ "ov_fortwars_build_enabled",
++ "1",
++ FCVAR_GAMEDLL,
++ "Allows ov_fortwars_spawn prop placement during the Fort Wars build phase." );
++
++struct OVModelChoice
++{
++ const char *pszKey;
++ const char *pszModel;
++ float flScale;
++ Vector mins;
++ Vector maxs;
++};
++
++static const OVModelChoice s_PropDisguises[] =
++{
++ { "can", "models/props_junk/garbage_metalcan001a.mdl", 0.85f, Vector( -10, -10, 0 ), Vector( 10, 10, 24 ) },
++ { "crate", "models/props_junk/wood_crate001a.mdl", 0.90f, Vector( -18, -18, 0 ), Vector( 18, 18, 42 ) },
++ { "barrel", "models/props_c17/oildrum001.mdl", 0.85f, Vector( -16, -16, 0 ), Vector( 16, 16, 48 ) },
++ { "chair", "models/props_c17/FurnitureChair001a.mdl", 0.90f, Vector( -18, -18, 0 ), Vector( 18, 18, 42 ) },
++ { "bucket", "models/props_junk/MetalBucket01a.mdl", 0.90f, Vector( -12, -12, 0 ), Vector( 12, 12, 28 ) },
++};
++
++static const OVModelChoice s_FortWarsProps[] =
++{
++ { "crate", "models/props_junk/wood_crate001a.mdl", 1.0f, Vector( 0, 0, 0 ), Vector( 0, 0, 0 ) },
++ { "barrel", "models/props_c17/oildrum001.mdl", 1.0f, Vector( 0, 0, 0 ), Vector( 0, 0, 0 ) },
++ { "pallet", "models/props_junk/wood_pallet001a.mdl", 1.0f, Vector( 0, 0, 0 ), Vector( 0, 0, 0 ) },
++ { "fence", "models/props_wasteland/wood_fence01a.mdl", 1.0f, Vector( 0, 0, 0 ), Vector( 0, 0, 0 ) },
++ { "sheet", "models/props_c17/FurnitureMattress001a.mdl", 1.0f, Vector( 0, 0, 0 ), Vector( 0, 0, 0 ) },
++};
++
++static const OVModelChoice *OV_FindModelChoice( const OVModelChoice *pChoices, int nChoices, const char *pszKey )
++{
++ for ( int i = 0; i < nChoices; ++i )
++ {
++ if ( !Q_stricmp( pszKey, pChoices[i].pszKey ) || !Q_stricmp( pszKey, pChoices[i].pszModel ) )
++ return &pChoices[i];
++ }
++ return NULL;
++}
++
++static bool OV_ExtractJsonBool( const char *pszJson, const char *pszKey )
++{
++ char szNeedle[128];
++ Q_snprintf( szNeedle, sizeof( szNeedle ), "\"%s\":true", pszKey );
++ return Q_strstr( pszJson, szNeedle ) != NULL;
++}
++
++static void OV_GetPlayerSteamIDString( CHL2MP_Player *pPlayer, char *pszOut, size_t nOut )
++{
++ CSteamID steamID;
++ if ( pPlayer && pPlayer->GetSteamID( &steamID ) )
++ {
++ Q_snprintf( pszOut, nOut, "%llu", steamID.ConvertToUint64() );
++ return;
++ }
++
++ Q_strncpy( pszOut, "0", nOut );
++}
++
++class COpenVibeJoinValidation
++{
++public:
++ COpenVibeJoinValidation() :
++ m_hRequest( INVALID_HTTPREQUEST_HANDLE ),
++ m_iUserID( -1 )
++ {
++ }
++
++ void Validate( CHL2MP_Player *pPlayer, const char *pszToken )
++ {
++ if ( !pPlayer || !pszToken || !pszToken[0] )
++ return;
++
++ if ( !SteamGameServerHTTP() )
++ {
++ Warning( "[OV Join] SteamGameServerHTTP unavailable; cannot validate join token.\n" );
++ if ( ov_join_required.GetBool() )
++ KickUser( pPlayer->GetUserID(), "OpenVibe backend validation unavailable" );
++ return;
++ }
++
++ char szSteamID[32];
++ OV_GetPlayerSteamIDString( pPlayer, szSteamID, sizeof( szSteamID ) );
++
++ char szUrl[512];
++ Q_snprintf( szUrl, sizeof( szUrl ), "%s/v1/travel/validate", ov_api_url.GetString() );
++
++ char szBody[512];
++ Q_snprintf(
++ szBody,
++ sizeof( szBody ),
++ "{\"token\":\"%s\",\"steamId\":\"%s\",\"serverId\":\"%s\"}",
++ pszToken,
++ szSteamID,
++ ov_server_id.GetString() );
++
++ ReleaseRequest();
++ m_hRequest = SteamGameServerHTTP()->CreateHTTPRequest( k_EHTTPMethodPOST, szUrl );
++ if ( m_hRequest == INVALID_HTTPREQUEST_HANDLE )
++ {
++ Warning( "[OV Join] Could not create validate request.\n" );
++ return;
++ }
++
++ m_iUserID = pPlayer->GetUserID();
++ SteamGameServerHTTP()->SetHTTPRequestHeaderValue( m_hRequest, "Accept", "application/json" );
++ SteamGameServerHTTP()->SetHTTPRequestRawPostBody(
++ m_hRequest,
++ "application/json",
++ (uint8 *)szBody,
++ Q_strlen( szBody ) );
++ SteamGameServerHTTP()->SetHTTPRequestNetworkActivityTimeout( m_hRequest, 8 );
++ SteamGameServerHTTP()->SetHTTPRequestAbsoluteTimeoutMS( m_hRequest, 10000 );
++
++ SteamAPICall_t hCall = k_uAPICallInvalid;
++ if ( !SteamGameServerHTTP()->SendHTTPRequest( m_hRequest, &hCall ) || hCall == k_uAPICallInvalid )
++ {
++ Warning( "[OV Join] Could not send validate request.\n" );
++ ReleaseRequest();
++ return;
++ }
++
++ m_Callback.Set( hCall, this, &COpenVibeJoinValidation::OnValidateResponse );
++ Msg( "[OV Join] Validating token for userID=%d steamID=%s server=%s.\n", m_iUserID, szSteamID, ov_server_id.GetString() );
++ }
++
++private:
++ void OnValidateResponse( HTTPRequestCompleted_t *pResult, bool bIOFailure )
++ {
++ char szBody[2048];
++ szBody[0] = '\0';
++
++ if ( !bIOFailure && pResult && pResult->m_bRequestSuccessful )
++ {
++ uint32 nBodySize = 0;
++ if ( SteamGameServerHTTP()->GetHTTPResponseBodySize( pResult->m_hRequest, &nBodySize ) &&
++ nBodySize > 0 &&
++ nBodySize < sizeof( szBody ) )
++ {
++ if ( SteamGameServerHTTP()->GetHTTPResponseBodyData(
++ pResult->m_hRequest,
++ (uint8 *)szBody,
++ nBodySize ) )
++ {
++ szBody[nBodySize] = '\0';
++ }
++ }
++ }
++
++ const bool bValid = OV_ExtractJsonBool( szBody, "valid" );
++ if ( bValid )
++ {
++ Msg( "[OV Join] Join token accepted for userID=%d.\n", m_iUserID );
++ }
++ else
++ {
++ Warning( "[OV Join] Join token rejected for userID=%d: %s\n", m_iUserID, szBody );
++ if ( ov_join_required.GetBool() )
++ KickUser( m_iUserID, "Invalid OpenVibe join token" );
++ }
++
++ ReleaseRequest();
++ }
++
++ void ReleaseRequest()
++ {
++ if ( m_hRequest != INVALID_HTTPREQUEST_HANDLE && SteamGameServerHTTP() )
++ {
++ SteamGameServerHTTP()->ReleaseHTTPRequest( m_hRequest );
++ }
++ m_hRequest = INVALID_HTTPREQUEST_HANDLE;
++ m_iUserID = -1;
++ }
++
++ void KickUser( int iUserID, const char *pszReason )
++ {
++ if ( iUserID >= 0 )
++ engine->ServerCommand( UTIL_VarArgs( "kickid %d \"%s\"\n", iUserID, pszReason ) );
++ }
++
++ HTTPRequestHandle m_hRequest;
++ int m_iUserID;
++ CCallResult< COpenVibeJoinValidation, HTTPRequestCompleted_t > m_Callback;
++};
++
++static COpenVibeJoinValidation g_OpenVibeJoinValidation;
++
++void OpenVibe_OnClientActive( CHL2MP_Player *pPlayer )
++{
++ if ( !pPlayer )
++ return;
++
++ const int iClient = engine->IndexOfEdict( pPlayer->edict() );
++ const char *pszToken = engine->GetClientConVarValue( iClient, "ov_join_token" );
++ char szSteamID[32];
++ OV_GetPlayerSteamIDString( pPlayer, szSteamID, sizeof( szSteamID ) );
++
++ if ( pszToken && pszToken[0] )
++ {
++ Msg( "[OV] ARRIVAL %s %s %s\n", szSteamID, ov_server_id.GetString(), pszToken );
++ g_OpenVibeJoinValidation.Validate( pPlayer, pszToken );
++ }
++ else if ( ov_join_required.GetBool() )
++ {
++ engine->ServerCommand( UTIL_VarArgs( "kickid %d \"OpenVibe join token required\"\n", pPlayer->GetUserID() ) );
++ }
++}
++
++static void OV_PropHuntDisguise_f( const CCommand &args )
++{
++ CHL2MP_Player *pPlayer = ToHL2MPPlayer( UTIL_GetCommandClient() );
++ if ( !pPlayer )
++ return;
++
++ if ( args.ArgC() < 2 )
++ {
++ ClientPrint( pPlayer, HUD_PRINTTALK, "Usage: ov_prophunt_disguise <can|crate|barrel|chair|bucket>" );
++ return;
++ }
++
++ const OVModelChoice *pChoice = OV_FindModelChoice( s_PropDisguises, ARRAYSIZE( s_PropDisguises ), args[1] );
++ if ( !pChoice )
++ {
++ ClientPrint( pPlayer, HUD_PRINTTALK, "That prop disguise is not allowed." );
++ return;
++ }
++
++ CBaseEntity::PrecacheModel( pChoice->pszModel );
++ pPlayer->SetModel( pChoice->pszModel );
++ pPlayer->SetModelScale( pChoice->flScale );
++ UTIL_SetSize( pPlayer, pChoice->mins, pChoice->maxs );
++ pPlayer->RemoveAllItems( true );
++ ClientPrint( pPlayer, HUD_PRINTTALK, "Prop disguise active." );
++}
++
++static void OV_PropHuntResetDisguise_f( const CCommand &args )
++{
++ CHL2MP_Player *pPlayer = ToHL2MPPlayer( UTIL_GetCommandClient() );
++ if ( !pPlayer )
++ return;
++
++ pPlayer->SetModelScale( 1.0f );
++ UTIL_SetSize( pPlayer, VEC_HULL_MIN, VEC_HULL_MAX );
++ pPlayer->SetPlayerModel();
++ pPlayer->GiveDefaultItems();
++ ClientPrint( pPlayer, HUD_PRINTTALK, "Prop disguise cleared." );
++}
++
++static ConCommand ov_prophunt_disguise_cmd(
++ "ov_prophunt_disguise",
++ OV_PropHuntDisguise_f,
++ "Disguise as a whitelisted Prop Hunt object.",
++ FCVAR_GAMEDLL );
++
++static ConCommand ov_prophunt_reset_disguise_cmd(
++ "ov_prophunt_reset_disguise",
++ OV_PropHuntResetDisguise_f,
++ "Restore normal player model after Prop Hunt disguise.",
++ FCVAR_GAMEDLL );
++
++static void OV_FortWarsSpawn_f( const CCommand &args )
++{
++ CHL2MP_Player *pPlayer = ToHL2MPPlayer( UTIL_GetCommandClient() );
++ if ( !pPlayer )
++ return;
++
++ if ( !ov_fortwars_build_enabled.GetBool() )
++ {
++ ClientPrint( pPlayer, HUD_PRINTTALK, "Fort Wars build phase is not active." );
++ return;
++ }
++
++ if ( args.ArgC() < 2 )
++ {
++ ClientPrint( pPlayer, HUD_PRINTTALK, "Usage: ov_fortwars_spawn <crate|barrel|pallet|fence|sheet>" );
++ return;
++ }
++
++ const OVModelChoice *pChoice = OV_FindModelChoice( s_FortWarsProps, ARRAYSIZE( s_FortWarsProps ), args[1] );
++ if ( !pChoice )
++ {
++ ClientPrint( pPlayer, HUD_PRINTTALK, "That Fort Wars prop is not allowed." );
++ return;
++ }
++
++ Vector vecForward;
++ AngleVectors( pPlayer->EyeAngles(), &vecForward );
++
++ trace_t tr;
++ const Vector vecStart = pPlayer->EyePosition();
++ const Vector vecEnd = vecStart + vecForward * 220.0f;
++ UTIL_TraceLine( vecStart, vecEnd, MASK_SOLID, pPlayer, COLLISION_GROUP_NONE, &tr );
++
++ Vector vecSpawn = tr.endpos;
++ if ( tr.DidHit() )
++ vecSpawn += tr.plane.normal * 24.0f;
++
++ CBaseEntity::PrecacheModel( pChoice->pszModel );
++
++ CBaseEntity *pProp = CreateEntityByName( "prop_physics_override" );
++ if ( !pProp )
++ {
++ ClientPrint( pPlayer, HUD_PRINTTALK, "Could not create prop." );
++ return;
++ }
++
++ pProp->KeyValue( "model", pChoice->pszModel );
++ pProp->KeyValue( "spawnflags", "256" );
++ pProp->SetAbsOrigin( vecSpawn );
++ pProp->SetAbsAngles( QAngle( 0.0f, pPlayer->EyeAngles().y, 0.0f ) );
++ pProp->SetOwnerEntity( pPlayer );
++
++ DispatchSpawn( pProp );
++ pProp->Activate();
++
++ ClientPrint( pPlayer, HUD_PRINTTALK, "Fort Wars prop placed." );
++}
++
++static ConCommand ov_fortwars_spawn_cmd(
++ "ov_fortwars_spawn",
++ OV_FortWarsSpawn_f,
++ "Spawn a whitelisted Fort Wars build prop in front of the player.",
++ FCVAR_GAMEDLL );
ddiff --git a/tools/apply-openvibe-sdk.sh b/tools/apply-openvibe-sdk.sh
nnew file mode 100755
iindex 0000000..d148ce0
---- /dev/null
++++ b/tools/apply-openvibe-sdk.sh
@@@ -0,0 +1,45 @@
++#!/usr/bin/env bash
++set -euo pipefail
++
++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
++SDK="${OPENVIBE_SDK:-$ROOT/engine/source-sdk-2013}"
++
++if [[ ! -d "$SDK/src/game/client/hl2mp" || ! -d "$SDK/src/game/server/hl2mp" ]]; then
++ echo "Source SDK 2013 checkout not found at $SDK" >&2
++ echo "Set OPENVIBE_SDK=/path/to/source-sdk-2013 if it lives elsewhere." >&2
++ exit 1
++fi
++
++copy_file() {
++ local src="$1"
++ local dst="$2"
++ install -D -m 0644 "$src" "$dst"
++ echo "[openvibe-sdk] copied ${dst#$SDK/}"
++}
++
++copy_file "$ROOT/sdk/openvibe/client/hl2mp/openvibe_client.cpp" \
++ "$SDK/src/game/client/hl2mp/openvibe_client.cpp"
++copy_file "$ROOT/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp" \
++ "$SDK/src/game/client/hl2mp/vgui_openvibe_menu.cpp"
++copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_server.cpp" \
++ "$SDK/src/game/server/hl2mp/openvibe_server.cpp"
++
++CLIENT_VPC="$SDK/src/game/client/client_hl2mp.vpc"
++SERVER_VPC="$SDK/src/game/server/server_hl2mp.vpc"
++HL2MP_CLIENT="$SDK/src/game/server/hl2mp/hl2mp_client.cpp"
++
++perl -0pi -e 's/^.*hl2mp\\openvibe_client\.cpp.*\n//mg; s/^.*hl2mp\\vgui_openvibe_menu\.cpp.*\n//mg; s/(\$File\s+"hl2mp\\clientmode_hl2mpnormal\.h"\n)/$1\t\t\t\$File\t"hl2mp\\openvibe_client.cpp"\n\t\t\t\$File\t"hl2mp\\vgui_openvibe_menu.cpp"\n/s' "$CLIENT_VPC"
++echo "[openvibe-sdk] patched client_hl2mp.vpc"
++
++perl -0pi -e 's/^.*hl2mp\\openvibe_server\.cpp.*\n//mg; s/(\$File\s+"hl2mp\\hl2mp_player\.h"\n)/$1\t\t\t\$File\t"hl2mp\\openvibe_server.cpp"\n/s' "$SERVER_VPC"
++echo "[openvibe-sdk] patched server_hl2mp.vpc"
++
++if ! grep -q 'OpenVibe_OnClientActive' "$HL2MP_CLIENT"; then
++ sed -i '/void Host_Say/a void OpenVibe_OnClientActive( CHL2MP_Player *pPlayer );' "$HL2MP_CLIENT"
++ sed -i '/FinishClientPutInServer( pPlayer );/a \tOpenVibe_OnClientActive( pPlayer );' "$HL2MP_CLIENT"
++ echo "[openvibe-sdk] patched hl2mp_client.cpp arrival hook"
++fi
++
++perl -0pi -e 's/[^\S\r\n]*\x0boid OpenVibe_OnClientActive/void OpenVibe_OnClientActive/g; s/\\tOpenVibe_OnClientActive/\tOpenVibe_OnClientActive/g; s/^[ \t]*void OpenVibe_OnClientActive/void OpenVibe_OnClientActive/m' "$HL2MP_CLIENT"
++
++echo "[openvibe-sdk] Source SDK OpenVibe patch applied"
ddiff --git a/tools/build-sdk-linux.sh b/tools/build-sdk-linux.sh
iindex b6643a7..3e3fc79 100755
---- a/tools/build-sdk-linux.sh
++++ b/tools/build-sdk-linux.sh
@@@ -6,6 +6,8 @@ SDK="$ROOT/engine/source-sdk-2013"
mkdir -p "$HOME/.ccache"
++"$ROOT/tools/apply-openvibe-sdk.sh"
++
cd "$SDK/src"
dos2unix buildallprojects sdk_container >/dev/null 2>&1 || true
chmod +x buildallprojects sdk_container >/dev/null 2>&1 || true
ddiff --git a/tools/compile-dev-maps-wine.sh b/tools/compile-dev-maps-wine.sh
iindex 4b8db9e..06ea87f 100755
---- a/tools/compile-dev-maps-wine.sh
++++ b/tools/compile-dev-maps-wine.sh
@@@ -6,10 +6,9 @@
set -euo pipefail
ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
--MAPS_DIR="$ROOT/game/openvibe.games/maps"
# Configuration
--TF2_INSTALL_DIR="${TF2_INSTALL_DIR:-$HOME/.steam/steam/SteamApps/common/Team Fortress 2}"
++TF2_INSTALL_DIR="${TF2_INSTALL_DIR:-$HOME/.steam/steam/steamapps/common/Team Fortress 2}"
VBSP_TOOL="$TF2_INSTALL_DIR/bin/vbsp.exe"
VVIS_TOOL="$TF2_INSTALL_DIR/bin/vvis.exe"
VRAD_TOOL="$TF2_INSTALL_DIR/bin/vrad.exe"
@@@ -47,46 +46,6 @@ generate_vmfs() {
echo "✓ VMF files generated"
}
--# Compile a single map (VMF -> BSP)
--compile_map() {
-- local map_name=$1
-- local vmf_file="$MAPS_DIR/$map_name.vmf"
-- local bsp_file="$MAPS_DIR/$map_name.bsp"
--
-- if [ ! -f "$vmf_file" ]; then
-- echo "⚠️ VMF not found: $vmf_file (skipping)"
-- return 1
-- fi
--
-- echo "🔨 Compiling $map_name..."
--
-- # VBSP: Geometry compilation
-- echo " → Running VBSP..."
-- WINEARCH=win32 wine "$VBSP_TOOL" -game "$TF2_INSTALL_DIR/tf" "$vmf_file" > /dev/null 2>&1 || {
-- echo "❌ VBSP failed for $map_name"
-- return 1
-- }
--
-- # VVIS: Visibility optimization (optional, can be slow)
-- if [ "${VVIS_ENABLED:-0}" != "0" ]; then
-- echo " → Running VVIS (this may take a while)..."
-- WINEARCH=win32 wine "$VVIS_TOOL" -game "$TF2_INSTALL_DIR/tf" "$bsp_file" > /dev/null 2>&1 || {
-- echo "⚠️ VVIS warning for $map_name (continuing anyway)"
-- }
-- else
-- echo " → Skipping VVIS (enable with VVIS_ENABLED=1)"
-- fi
--
-- # VRAD: Lighting compilation
-- echo " → Running VRAD..."
-- WINEARCH=win32 wine "$VRAD_TOOL" -game "$TF2_INSTALL_DIR/tf" -final "$bsp_file" > /dev/null 2>&1 || {
-- echo "❌ VRAD failed for $map_name"
-- return 1
-- }
--
-- echo "✓ Compiled: $bsp_file"
--}
--
# Compile all maps
compile_all_maps() {
local maps=(
@@@ -99,7 +58,7 @@ compile_all_maps() {
echo "🎬 Compiling all maps..."
for map in "${maps[@]}"; do
-- compile_map "$map" || echo "⚠️ Failed to compile $map"
++ "$ROOT/tools/compile-map-wine.sh" "$map" || echo "⚠️ Failed to compile $map"
done
echo "✓ Map compilation complete"
}
@@@ -119,7 +78,7 @@ main() {
compile_all_maps
echo ""
-- echo "✅ All done! BSP files ready in: $MAPS_DIR"
++ echo "✅ All done! BSP files ready in: $ROOT/game/openvibe.games/maps"
echo ""
echo "Next steps:"
echo " 1. Copy BSP files to your SRCDS installation"
ddiff --git a/tools/dev-down.sh b/tools/dev-down.sh
iindex 3fd3225..6877148 100755
---- a/tools/dev-down.sh
++++ b/tools/dev-down.sh
@@@ -1,8 +1,47 @@
#!/usr/bin/env bash
set -euo pipefail
--for session in ov-api ov-hub ov-prophunt ov-deathrun ov-fortwars ov-traitortown; do
++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
++MOD="$ROOT/game/openvibe.games"
++
++sessions=(
++ ov-api
++ ov-hub
++ ov-prophunt
++ ov-deathrun
++ ov-fortwars
++ ov-traitortown
++ ov-sidecar-hub
++ ov-sidecar-prophunt
++ ov-sidecar-deathrun
++ ov-sidecar-fortwars
++ ov-sidecar-traitortown
++)
++
++for session in "${sessions[@]}"; do
tmux kill-session -t "$session" 2>/dev/null || true
done
--"${OPENVIBE_ROOT:-$HOME/src/openvibe-source}/tools/dev-db-down.sh"
++kill_matching() {
++ local signal="$1"
++ local pattern="$2"
++ pgrep -f "$pattern" 2>/dev/null | while read -r pid; do
++ if [[ "$pid" != "$$" ]]; then
++ kill "-$signal" "$pid" 2>/dev/null || true
++ fi
++ done
++}
++
++kill_matching TERM "srcds_linux64 -game $MOD"
++kill_matching TERM "$ROOT/tools/ov-sidecar.mjs"
++kill_matching TERM "$ROOT/tools/dev-api.sh"
++kill_matching TERM "$ROOT/backend/node_modules/.bin/tsx watch src/index.ts"
++
++sleep 1
++
++kill_matching KILL "srcds_linux64 -game $MOD"
++kill_matching KILL "$ROOT/tools/ov-sidecar.mjs"
++kill_matching KILL "$ROOT/tools/dev-api.sh"
++kill_matching KILL "$ROOT/backend/node_modules/.bin/tsx watch src/index.ts"
++
++"$ROOT/tools/dev-db-down.sh"
ddiff --git a/tools/generate-dev-vmfs.mjs b/tools/generate-dev-vmfs.mjs
iindex 9616b2b..dadadfd 100644
---- a/tools/generate-dev-vmfs.mjs
++++ b/tools/generate-dev-vmfs.mjs
@@@ -348,7 +348,7 @@ function hub() {
[pad.origin[0] - 128, pad.origin[1] - 128, 16],
[pad.origin[0] + 128, pad.origin[1] + 128, 128],
commandTarget,
-- `connect 127.0.0.1:${pad.port}`,
++ `ov_join ${pad.mode}`,
),
];
}),
@@@ -417,7 +417,7 @@ function minigame(mapName, modeName, returnX = 0) {
[returnX - 128, 580, 16],
[returnX + 128, 836, 128],
"cmd_return_hub",
-- "connect 127.0.0.1:27015",
++ "ov_join hub",
),
];
ddiff --git a/tools/run-server.sh b/tools/run-server.sh
iindex b911d39..de76475 100755
---- a/tools/run-server.sh
++++ b/tools/run-server.sh
@@@ -15,6 +15,7 @@ CFG="$5"
ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
SRCDS="${OPENVIBE_SRCDS:-$HOME/srcds/tf2}"
MOD="$ROOT/game/openvibe.games"
++MAP_DELAY="${OPENVIBE_SRCDS_MAP_DELAY:-6}"
if [[ ! -x "$SRCDS/srcds_linux64" ]]; then
echo "Missing 64-bit SRCDS at $SRCDS/srcds_linux64" >&2
@@@ -30,14 +31,35 @@ fi
cd "$SRCDS"
++printf '243750\n' > "$SRCDS/steam_appid.txt"
++
export LD_LIBRARY_PATH="$MOD/bin/linux64:.:$SRCDS/bin/linux64:$SRCDS/bin:${LD_LIBRARY_PATH:-}"
--exec ./srcds_linux64 \
-- -game "$MOD" \
-- -console \
-- -usercon \
-- +ip 0.0.0.0 \
-- +port "$PORT" \
-- +maxplayers "$MAXPLAYERS" \
-- +map "$MAP" \
++server_cmd=(
++ ./srcds_linux64
++ -game "$MOD"
++ -console
++ -usercon
++ -condebug
++ +ip 0.0.0.0
++ +port "$PORT"
++ +maxplayers "$MAXPLAYERS"
+exec "$CFG"
++)
++
++if [[ "${OPENVIBE_SRCDS_PIPE_MAP:-1}" == "1" ]]; then
++ printf -v server_command '%q ' "${server_cmd[@]}"
++ map_input() {
++ sleep "$MAP_DELAY"
++ printf 'map %s\n' "$MAP"
++ while sleep 3600; do :; done
++ }
++
++ if command -v script >/dev/null 2>&1; then
++ map_input | script -qfec "$server_command" /dev/null
++ else
++ map_input | "${server_cmd[@]}"
++ fi
++else
++ exec "${server_cmd[@]}" +map "$MAP"
++fi
ddiff --git a/tools/smoke-srcds.sh b/tools/smoke-srcds.sh
iindex d8636cf..0d030f7 100755
---- a/tools/smoke-srcds.sh
++++ b/tools/smoke-srcds.sh
@@@ -12,7 +12,7 @@ run_check() {
local log="/tmp/openvibe-srcds-$name.log"
set +e
-- timeout 30s "$ROOT/tools/$script" >"$log" 2>&1
++ timeout "${OPENVIBE_SRCDS_SMOKE_TIMEOUT:-70}s" "$ROOT/tools/$script" >"$log" 2>&1
local code=$?
set -e
@@@ -28,7 +28,13 @@ run_check() {
exit 1
fi
-- if ! grep -Eq "Started map \"$map\"|Spawn Server: $map|Loading map \"$map\"" "$log"; then
++ if grep -Eiq "AN ERROR HAS OCCURRED|Error running script named" "$log"; then
++ echo "[srcds:$name] reported a VScript startup error"
++ tail -160 "$log"
++ exit 1
++ fi
++
++ if ! grep -Eq "Started map \"$map\"|Spawn Server: $map|Loading map \"$map\"|SV_ActivateServer" "$log"; then
echo "[srcds:$name] did not reach map load for $map"
tail -120 "$log"
exit 1