openvibers/source.openvibe.games Commits
Implement OpenVibe next steps foundation
Changes
@@@ -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/*.loghammer/vmf/*.prthammer/vmf/*.linhammer/vmf/*.vmxhammer/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++*.corecoredebug.log@@@ -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 servicescd ~/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.@@@ -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----```++```textopenvibe-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/backendnpm 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@@@ -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 buildnpm 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`. |@@@ -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```bashcd ~/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```bashcd ~/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-sourcenode tools/generate-dev-vmfs.mjs----# Compile all maps via Wine + TF2 toolstools/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.@@@ -2,5 +2,11 @@ HOST=127.0.0.1PORT=3000DATABASE_URL=postgres://openvibe:[email protected]:5432/openvibeOPENVIBE_PUBLIC_URL=https://openvibe.games++OPENVIBE_CDN_BASE_URL=https://openvibe.games/cdnOPENVIBE_DEV_AUTH_ENABLED=trueOPENVIBE_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@@@ -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",@@@ -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"@@@ -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();@@@ -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;}@@@ -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";@@@ -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,});nnew file mode 100644@@@ -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();++ }++}@@@ -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@@@ -6,12 +6,12 @@ OpenVibe: Source uses Source dedicated servers for gameplay and OpenVibe.Games a```textSource 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. |@@@ -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.sogame/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.shtools/run-fortwars.shtools/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++```BBinary files a/game/openvibe.games/maps/dr_openvibe_dev.bsp and b/game/openvibe.games/maps/dr_openvibe_dev.bsp differBBinary files a/game/openvibe.games/maps/fw_openvibe_dev.bsp and b/game/openvibe.games/maps/fw_openvibe_dev.bsp differBBinary files a/game/openvibe.games/maps/ov_hub.bsp and b/game/openvibe.games/maps/ov_hub.bsp differBBinary files a/game/openvibe.games/maps/ph_openvibe_dev.bsp and b/game/openvibe.games/maps/ph_openvibe_dev.bsp differBBinary files a/game/openvibe.games/maps/tt_openvibe_dev.bsp and b/game/openvibe.games/maps/tt_openvibe_dev.bsp differBBinary files a/game/openvibe.games/modelsounds.cache and b/game/openvibe.games/modelsounds.cache differnnew file mode 100644@@@ -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"++ }++}@@@ -1,4 +1,4 @@--soundscapes_manifest++soundscaples_manifest{"file" "scripts/soundscapes_openvibe.txt"}@@@ -1 +1,4 @@--// OpenVibe: Source placeholder soundscape file.++"openvibe.blank"++{++ "dsp" "1"++}@@@ -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@@@ -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{@@@ -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{@@@ -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{@@@ -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{@@@ -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{nnew file mode 100644@@@ -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/;++ }++}@@@ -23,7 +23,13 @@ services:restart: unless-stoppedenvironment:DATABASE_URL: postgres://openvibe:openvibe@postgres:5432/openvibe++ SESSION_REDIS_URL: redis://redis:6379OPENVIBE_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_startedhealthcheck:test: ["CMD-SHELL", "curl -fs http://localhost:3000/health || exit 1"]interval: 10stimeout: 5sretries: 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:nnew file mode 100644@@@ -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: 80nnew file mode 100644@@@ -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@@@ -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 }; }});nnew file mode 100644@@@ -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 );nnew file mode 100644@@@ -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 );nnew file mode 100644@@@ -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 );nnew file mode 100755@@@ -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"@@@ -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 || truechmod +x buildallprojects sdk_container >/dev/null 2>&1 || true@@@ -6,10 +6,9 @@set -euo pipefailROOT="${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 mapscompile_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"doneecho "✓ Map compilation complete"}@@@ -119,7 +78,7 @@ main() {compile_all_mapsecho ""-- 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"@@@ -1,8 +1,47 @@#!/usr/bin/env bashset -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[@]}"; dotmux kill-session -t "$session" 2>/dev/null || truedone--"${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"@@@ -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",),];@@@ -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" ]]; thenecho "Missing 64-bit SRCDS at $SRCDS/srcds_linux64" >&2@@@ -30,14 +31,35 @@ ficd "$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@@@ -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>&1local code=$?set -e@@@ -28,7 +28,13 @@ run_check() {exit 1fi-- 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"; thenecho "[srcds:$name] did not reach map load for $map"tail -120 "$log"exit 1
@@ -2,12 +2,24 @@ backend/node_modules/
@@ -2,12 +2,24 @@ backend/node_modules/
backend/dist/
backend/dist/
backend/.env
backend/.env
+# Local Valve SDK checkout. OpenVibe patch sources are tracked in sdk/openvibe/.
+engine/source-sdk-2013/
+
hammer/vmf/*.log
hammer/vmf/*.log
hammer/vmf/*.prt
hammer/vmf/*.prt
hammer/vmf/*.lin
hammer/vmf/*.lin
hammer/vmf/*.vmx
hammer/vmf/*.vmx
hammer/vmf/*.bsp
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
core
core
debug.log
debug.log
@@ -1,138 +1,86 @@
@@ -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
```bash
-# Terminal 1: Start all backend services
cd ~/src/openvibe-source/backend
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
```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.
@@ -1,422 +1,108 @@
@@ -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/
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
```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 run build
-npm test # Should pass 6 tests
-docker build -t openvibe_api .
+npm test
```
```
-### Compile Maps
+Source SDK:
+
```bash
```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
```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
```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
@@ -1,411 +1,139 @@
@@ -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
```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
npm test
```
```
-### 3. Map Compilation (30-60 minutes, one-time)
```bash
```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
```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
```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
```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
```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
```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
```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`. |
@@ -1,10 +1,11 @@
@@ -1,10 +1,11 @@
# OpenVibe: Source
# 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`
- Public identity: `OpenVibe.Games`
- Source mod folder: `game/openvibe.games`
- Source mod folder: `game/openvibe.games`
- Backend/API: `backend/`
- Backend/API: `backend/`
+- Tracked OpenVibe C++ patch sources: `sdk/openvibe/`
- VScript game logic: `game/openvibe.games/scripts/vscripts/`
- VScript game logic: `game/openvibe.games/scripts/vscripts/`
- Editable maps: `hammer/vmf/`
- Editable maps: `hammer/vmf/`
- Compiled maps: `game/openvibe.games/maps/`
- Compiled maps: `game/openvibe.games/maps/`
@@ -12,170 +13,168 @@ Linux-first Source SDK 2013 multiplayer project for `OpenVibe: Source`.
@@ -12,170 +13,168 @@ Linux-first Source SDK 2013 multiplayer project for `OpenVibe: Source`.
## Current State
## 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
## 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
```bash
cd ~/src/openvibe-source/backend
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
```bash
cd ~/src/openvibe-source
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
```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
```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
```bash
-# Regenerate VMFs (adds VScript entities, HUD text, team spawns)
+cd ~/src/openvibe-source
node tools/generate-dev-vmfs.mjs
node tools/generate-dev-vmfs.mjs
-
-# Compile all maps via Wine + TF2 tools
tools/compile-dev-maps-wine.sh
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
```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 |
| Method | Path | Description |
-|--------|------|-------------|
+| --- | --- | --- |
| `GET` | `/health` | Health check |
| `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/me?steamId=` | Player profile + inventory + shop |
| `GET` | `/v1/shop` | Shop catalog |
| `GET` | `/v1/shop` | Shop catalog |
+| `GET` | `/v1/assets/manifest` | CDN-backed cosmetic asset manifest |
| `POST` | `/v1/shop/buy` | Purchase item |
| `POST` | `/v1/shop/buy` | Purchase item |
| `POST` | `/v1/equip` | Equip owned item |
| `POST` | `/v1/equip` | Equip owned item |
| `GET` | `/v1/leaderboard` | Top players by XP |
| `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
@@ -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/matches/end` | Record match result + reward |
| `POST` | `/v1/admin/shop/items` | Admin: upsert shop item |
| `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.
@@ -2,5 +2,11 @@ HOST=127.0.0.1
@@ -2,5 +2,11 @@ HOST=127.0.0.1
PORT=3000
PORT=3000
DATABASE_URL=postgres://openvibe:[email protected]:5432/openvibe
DATABASE_URL=postgres://openvibe:[email protected]:5432/openvibe
OPENVIBE_PUBLIC_URL=https://openvibe.games
OPENVIBE_PUBLIC_URL=https://openvibe.games
+OPENVIBE_CDN_BASE_URL=https://openvibe.games/cdn
OPENVIBE_DEV_AUTH_ENABLED=true
OPENVIBE_DEV_AUTH_ENABLED=true
OPENVIBE_DEFAULT_SERVER_SECRET=dev-secret
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
@@ -11,6 +11,7 @@
@@ -11,6 +11,7 @@
"@fastify/cors": "^11.1.0",
"@fastify/cors": "^11.1.0",
"dotenv": "^17.2.3",
"dotenv": "^17.2.3",
"fastify": "^5.6.2",
"fastify": "^5.6.2",
+ "ioredis": "^5.11.1",
"nanoid": "^5.1.6",
"nanoid": "^5.1.6",
"pg": "^8.16.3",
"pg": "^8.16.3",
"zod": "^4.2.1"
"zod": "^4.2.1"
@@ -633,6 +634,12 @@
@@ -633,6 +634,12 @@
"ipaddr.js": "^2.1.0"
"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": {
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@@ -1223,6 +1230,15 @@
@@ -1223,6 +1230,15 @@
"node": ">=18"
"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": {
"node_modules/convert-source-map": {
"version": "2.0.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1243,6 +1259,32 @@
@@ -1243,6 +1259,32 @@
"url": "https://opencollective.com/express"
"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": {
"node_modules/dequal": {
"version": "2.0.3",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -1509,6 +1551,28 @@
@@ -1509,6 +1551,28 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
"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": {
"node_modules/ipaddr.js": {
"version": "2.4.0",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
@@ -1863,6 +1927,12 @@
@@ -1863,6 +1927,12 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
"@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": {
"node_modules/nanoid": {
"version": "5.1.16",
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
@@ -2175,6 +2245,27 @@
@@ -2175,6 +2245,27 @@
"node": ">= 12.13.0"
"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": {
"node_modules/require-from-string": {
"version": "2.0.2",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -2350,6 +2441,12 @@
@@ -2350,6 +2441,12 @@
"dev": true,
"dev": true,
"license": "MIT"
"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": {
"node_modules/std-env": {
"version": "4.1.0",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
@@ -16,6 +16,7 @@
@@ -16,6 +16,7 @@
"@fastify/cors": "^11.1.0",
"@fastify/cors": "^11.1.0",
"dotenv": "^17.2.3",
"dotenv": "^17.2.3",
"fastify": "^5.6.2",
"fastify": "^5.6.2",
+ "ioredis": "^5.11.1",
"nanoid": "^5.1.6",
"nanoid": "^5.1.6",
"pg": "^8.16.3",
"pg": "^8.16.3",
"zod": "^4.2.1"
"zod": "^4.2.1"
@@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it } from "vitest";
import { createApp } from "./app.js";
import { createApp } from "./app.js";
import { MemoryOpenVibeRepository } from "./repository-memory.js";
import { MemoryOpenVibeRepository } from "./repository-memory.js";
+import { SessionInput } from "./sessions.js";
const steamId = "76561198000000000";
const steamId = "76561198000000000";
const serverSecret = "dev-secret";
const serverSecret = "dev-secret";
@@ -33,6 +34,73 @@ describe("OpenVibe API vertical slice", () => {
@@ -33,6 +34,73 @@ describe("OpenVibe API vertical slice", () => {
await app.close();
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 () => {
it("registers servers and reserves a one-use travel token", async () => {
const app = await testApp();
const app = await testApp();
@@ -4,6 +4,7 @@ import { ZodError } from "zod";
@@ -4,6 +4,7 @@ import { ZodError } from "zod";
import { OpenVibeRepository } from "./domain.js";
import { OpenVibeRepository } from "./domain.js";
import {
import {
authDevSchema,
authDevSchema,
+ authSteamSchema,
buyItemSchema,
buyItemSchema,
equipItemSchema,
equipItemSchema,
getMeQuerySchema,
getMeQuerySchema,
@@ -17,11 +18,13 @@ import {
@@ -17,11 +18,13 @@ import {
validateJoinTokenSchema,
validateJoinTokenSchema,
} from "./schemas.js";
} from "./schemas.js";
import { RepositoryError } from "./repository-memory.js";
import { RepositoryError } from "./repository-memory.js";
+import { createSessionToken, OpenVibeSessionStore } from "./sessions.js";
export interface AppOptions {
export interface AppOptions {
repository: OpenVibeRepository;
repository: OpenVibeRepository;
devAuthEnabled?: boolean;
devAuthEnabled?: boolean;
adminSecret?: string;
adminSecret?: string;
+ sessionStore?: OpenVibeSessionStore;
}
}
export async function createApp(options: AppOptions): Promise<FastifyInstance> {
export async function createApp(options: AppOptions): Promise<FastifyInstance> {
@@ -78,18 +81,79 @@ 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 body = authDevSchema.parse(request.body ?? {});
const profile = await options.repository.upsertDevPlayer(body);
const profile = await options.repository.upsertDevPlayer(body);
+ const sessionToken = await createSessionToken(options.sessionStore, "dev", profile.player.steamId);
return {
return {
- sessionToken: `dev.${profile.player.steamId}`,
+ sessionToken,
...profile,
...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) => {
app.get("/v1/me", async (request, reply) => {
@@ -103,6 +167,29 @@ export async function createApp(options: AppOptions): Promise<FastifyInstance> {
@@ -103,6 +167,29 @@ export async function createApp(options: AppOptions): Promise<FastifyInstance> {
items: await options.repository.listShop(),
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) => {
app.post("/v1/shop/buy", async (request) => {
const body = buyItemSchema.parse(request.body);
const body = buyItemSchema.parse(request.body);
return options.repository.buyItem(body);
return options.repository.buyItem(body);
@@ -170,5 +257,9 @@ export async function createApp(options: AppOptions): Promise<FastifyInstance> {
@@ -170,5 +257,9 @@ export async function createApp(options: AppOptions): Promise<FastifyInstance> {
return options.repository.upsertShopItem(body);
return options.repository.upsertShopItem(body);
});
});
+ app.addHook("onClose", async () => {
+ await options.sessionStore?.close?.();
+ });
+
return app;
return app;
}
}
@@ -2,6 +2,7 @@ import "dotenv/config";
@@ -2,6 +2,7 @@ import "dotenv/config";
import { Pool } from "pg";
import { Pool } from "pg";
import { createApp } from "./app.js";
import { createApp } from "./app.js";
import { PgOpenVibeRepository } from "./repository-pg.js";
import { PgOpenVibeRepository } from "./repository-pg.js";
+import { RedisSessionStore } from "./sessions.js";
const pool = new Pool({
const pool = new Pool({
connectionString:
connectionString:
@@ -11,6 +12,9 @@ const pool = new Pool({
@@ -11,6 +12,9 @@ const pool = new Pool({
const app = await createApp({
const app = await createApp({
repository: new PgOpenVibeRepository(pool),
repository: new PgOpenVibeRepository(pool),
devAuthEnabled: process.env.OPENVIBE_DEV_AUTH_ENABLED !== "false",
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";
const host = process.env.HOST ?? "127.0.0.1";
@@ -11,6 +11,12 @@ export const authDevSchema = z.object({
@@ -11,6 +11,12 @@ export const authDevSchema = z.object({
displayName: z.string().trim().min(1).max(64).default("OpenVibe Dev"),
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({
export const getMeQuerySchema = z.object({
steamId,
steamId,
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,65 @@
@@ -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();
+ }
+}
@@ -1,396 +1,134 @@
@@ -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
@@ -6,12 +6,12 @@ OpenVibe: Source uses Source dedicated servers for gameplay and OpenVibe.Games a
@@ -6,12 +6,12 @@ OpenVibe: Source uses Source dedicated servers for gameplay and OpenVibe.Games a
```text
```text
Source client
Source client
- → OpenVibe main menu
+ → OpenVibe VGUI menu / launcher
→ Steam auth / dev auth → OpenVibe.Games API
→ Steam auth / dev auth → OpenVibe.Games API
- → Hub SRCDS (portal pads → direct connect)
+ → Hub SRCDS (portal pads → ov_join)
Hub SRCDS ov_hub.nut
Hub SRCDS ov_hub.nut
- → portal pads (trigger → connect)
+ → portal pads (trigger → ov_join <mode>)
→ shop NPC / inventory station
→ shop NPC / inventory station
→ sidecar → /v1/servers/heartbeat
→ sidecar → /v1/servers/heartbeat
@@ -30,6 +30,7 @@ Sidecar (ov-sidecar.mjs)
@@ -30,6 +30,7 @@ Sidecar (ov-sidecar.mjs)
Backend API (Fastify/TypeScript)
Backend API (Fastify/TypeScript)
→ /v1/auth/{dev,steam}
→ /v1/auth/{dev,steam}
→ /v1/me /v1/shop /v1/equip
→ /v1/me /v1/shop /v1/equip
+ → /v1/assets/manifest
→ /v1/servers/{register,heartbeat}
→ /v1/servers/{register,heartbeat}
→ /v1/travel/{request,validate}
→ /v1/travel/{request,validate}
→ /v1/matches/end
→ /v1/matches/end
@@ -41,6 +42,12 @@ PostgreSQL
@@ -41,6 +42,12 @@ PostgreSQL
→ shop_items, player_items
→ shop_items, player_items
→ game_servers, join_tokens
→ game_servers, join_tokens
→ match_results
→ match_results
+
+Redis (optional)
+ → auth sessions
+
+CDN/static hosting
+ → cosmetic assets served under openvibe.games
```
```
## Modes
## Modes
@@ -61,6 +68,7 @@ PostgreSQL
@@ -61,6 +68,7 @@ PostgreSQL
- Server heartbeat and reward APIs require `serverSecret`.
- Server heartbeat and reward APIs require `serverSecret`.
- The backend owns item IDs, asset paths, prices, and equipped cosmetics.
- The backend owns item IDs, asset paths, prices, and equipped cosmetics.
- Admin endpoints require `X-Admin-Secret` header.
- 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
## VScript → API Event Protocol
@@ -77,26 +85,29 @@ VScript cannot make HTTP requests directly. The sidecar bridges the gap:
@@ -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` |
| `REWARD` | `matchId serverId secret uid mode coins xp` | `POST /v1/matches/end` |
| `SAY` | `message…` | log only |
| `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
```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. |
@@ -89,7 +89,20 @@ Z:\home\workstation\src\openvibe-source\game\openvibe.games\maps
@@ -89,7 +89,20 @@ Z:\home\workstation\src\openvibe-source\game\openvibe.games\maps
## Source Binaries
## 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:
Run:
@@ -114,11 +127,11 @@ game/openvibe.games/bin/linux64/client_srv.so
@@ -114,11 +127,11 @@ game/openvibe.games/bin/linux64/client_srv.so
game/openvibe.games/bin/linux64/server_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
## 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:
Installed location:
@@ -148,3 +161,17 @@ tools/run-deathrun.sh
@@ -148,3 +161,17 @@ tools/run-deathrun.sh
tools/run-fortwars.sh
tools/run-fortwars.sh
tools/run-traitortown.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
+```
Binary files a/game/openvibe.games/maps/dr_openvibe_dev.bsp and b/game/openvibe.games/maps/dr_openvibe_dev.bsp differ
Binary files a/game/openvibe.games/maps/dr_openvibe_dev.bsp and b/game/openvibe.games/maps/dr_openvibe_dev.bsp differ
Binary files a/game/openvibe.games/maps/fw_openvibe_dev.bsp and b/game/openvibe.games/maps/fw_openvibe_dev.bsp differ
Binary files a/game/openvibe.games/maps/fw_openvibe_dev.bsp and b/game/openvibe.games/maps/fw_openvibe_dev.bsp differ
Binary files a/game/openvibe.games/maps/ov_hub.bsp and b/game/openvibe.games/maps/ov_hub.bsp differ
Binary files a/game/openvibe.games/maps/ov_hub.bsp and b/game/openvibe.games/maps/ov_hub.bsp differ
Binary files a/game/openvibe.games/maps/ph_openvibe_dev.bsp and b/game/openvibe.games/maps/ph_openvibe_dev.bsp differ
Binary files a/game/openvibe.games/maps/ph_openvibe_dev.bsp and b/game/openvibe.games/maps/ph_openvibe_dev.bsp differ
Binary files a/game/openvibe.games/maps/tt_openvibe_dev.bsp and b/game/openvibe.games/maps/tt_openvibe_dev.bsp differ
Binary files a/game/openvibe.games/maps/tt_openvibe_dev.bsp and b/game/openvibe.games/maps/tt_openvibe_dev.bsp differ
Binary files a/game/openvibe.games/modelsounds.cache and b/game/openvibe.games/modelsounds.cache differ
Binary files a/game/openvibe.games/modelsounds.cache and b/game/openvibe.games/modelsounds.cache differ
new file mode 100644
new file mode 100644
@@ -0,0 +1,57 @@
@@ -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"
+ }
+}
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-soundscapes_manifest
+soundscaples_manifest
{
{
"file" "scripts/soundscapes_openvibe.txt"
"file" "scripts/soundscapes_openvibe.txt"
}
}
@@ -1 +1,4 @@
@@ -1 +1,4 @@
-// OpenVibe: Source placeholder soundscape file.
+"openvibe.blank"
+{
+ "dsp" "1"
+}
@@ -205,7 +205,7 @@ function OV_CreateThinkEnt(thinkFuncName) {
@@ -205,7 +205,7 @@ function OV_CreateThinkEnt(thinkFuncName) {
printl("[OV] ERROR: could not create think entity for " + thinkFuncName)
printl("[OV] ERROR: could not create think entity for " + thinkFuncName)
return null
return null
}
}
- ent.SetName("ov_think_" + thinkFuncName)
+ ent.__KeyValueFromString("targetname", "ov_think_" + thinkFuncName)
AddThinkToEnt(ent, thinkFuncName)
AddThinkToEnt(ent, thinkFuncName)
printl("[OV] think entity created for " + thinkFuncName)
printl("[OV] think entity created for " + thinkFuncName)
return ent
return ent
@@ -865,7 +865,7 @@ world
@@ -865,7 +865,7 @@ world
"wait" "1"
"wait" "1"
connections
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
solid
{
{
@@ -865,7 +865,7 @@ world
@@ -865,7 +865,7 @@ world
"wait" "1"
"wait" "1"
connections
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
solid
{
{
@@ -1080,7 +1080,7 @@ world
@@ -1080,7 +1080,7 @@ world
"wait" "1"
"wait" "1"
connections
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
solid
{
{
@@ -1178,7 +1178,7 @@ world
@@ -1178,7 +1178,7 @@ world
"wait" "1"
"wait" "1"
connections
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
solid
{
{
@@ -1276,7 +1276,7 @@ world
@@ -1276,7 +1276,7 @@ world
"wait" "1"
"wait" "1"
connections
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
solid
{
{
@@ -1374,7 +1374,7 @@ world
@@ -1374,7 +1374,7 @@ world
"wait" "1"
"wait" "1"
connections
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
solid
{
{
@@ -865,7 +865,7 @@ world
@@ -865,7 +865,7 @@ world
"wait" "1"
"wait" "1"
connections
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
solid
{
{
@@ -865,7 +865,7 @@ world
@@ -865,7 +865,7 @@ world
"wait" "1"
"wait" "1"
connections
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
solid
{
{
new file mode 100644
new file mode 100644
@@ -0,0 +1,32 @@
@@ -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/;
+ }
+}
@@ -23,7 +23,13 @@ services:
@@ -23,7 +23,13 @@ services:
restart: unless-stopped
restart: unless-stopped
environment:
environment:
DATABASE_URL: postgres://openvibe:openvibe@postgres:5432/openvibe
DATABASE_URL: postgres://openvibe:openvibe@postgres:5432/openvibe
+ SESSION_REDIS_URL: redis://redis:6379
OPENVIBE_DEV_AUTH_ENABLED: "true"
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"
HOST: "0.0.0.0"
PORT: "3000"
PORT: "3000"
ports:
ports:
@@ -31,11 +37,32 @@ services:
@@ -31,11 +37,32 @@ services:
depends_on:
depends_on:
postgres:
postgres:
condition: service_healthy
condition: service_healthy
+ redis:
+ condition: service_started
healthcheck:
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:3000/health || exit 1"]
test: ["CMD-SHELL", "curl -fs http://localhost:3000/health || exit 1"]
interval: 10s
interval: 10s
timeout: 5s
timeout: 5s
retries: 6
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:
volumes:
pg_data:
pg_data:
+ redis_data:
new file mode 100644
new file mode 100644
@@ -0,0 +1,339 @@
@@ -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
new file mode 100644
new file mode 100644
@@ -0,0 +1,79 @@
@@ -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
@@ -126,8 +126,8 @@ ipcMain.handle('api:leaderboard', async (_e, limit = 10) => {
@@ -126,8 +126,8 @@ ipcMain.handle('api:leaderboard', async (_e, limit = 10) => {
catch { return []; }
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 }; }
catch (e) { return { error: e.message }; }
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,452 @@
@@ -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 );
new file mode 100644
new file mode 100644
@@ -0,0 +1,143 @@
@@ -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 );
new file mode 100644
new file mode 100644
@@ -0,0 +1,364 @@
@@ -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 );
new file mode 100755
new file mode 100755
@@ -0,0 +1,45 @@
@@ -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"
@@ -6,6 +6,8 @@ SDK="$ROOT/engine/source-sdk-2013"
@@ -6,6 +6,8 @@ SDK="$ROOT/engine/source-sdk-2013"
mkdir -p "$HOME/.ccache"
mkdir -p "$HOME/.ccache"
+"$ROOT/tools/apply-openvibe-sdk.sh"
+
cd "$SDK/src"
cd "$SDK/src"
dos2unix buildallprojects sdk_container >/dev/null 2>&1 || true
dos2unix buildallprojects sdk_container >/dev/null 2>&1 || true
chmod +x buildallprojects sdk_container >/dev/null 2>&1 || true
chmod +x buildallprojects sdk_container >/dev/null 2>&1 || true
@@ -6,10 +6,9 @@
@@ -6,10 +6,9 @@
set -euo pipefail
set -euo pipefail
ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
-MAPS_DIR="$ROOT/game/openvibe.games/maps"
# Configuration
# 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"
VBSP_TOOL="$TF2_INSTALL_DIR/bin/vbsp.exe"
VVIS_TOOL="$TF2_INSTALL_DIR/bin/vvis.exe"
VVIS_TOOL="$TF2_INSTALL_DIR/bin/vvis.exe"
VRAD_TOOL="$TF2_INSTALL_DIR/bin/vrad.exe"
VRAD_TOOL="$TF2_INSTALL_DIR/bin/vrad.exe"
@@ -47,46 +46,6 @@ generate_vmfs() {
@@ -47,46 +46,6 @@ generate_vmfs() {
echo "✓ VMF files generated"
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
compile_all_maps() {
compile_all_maps() {
local maps=(
local maps=(
@@ -99,7 +58,7 @@ compile_all_maps() {
@@ -99,7 +58,7 @@ compile_all_maps() {
echo "🎬 Compiling all maps..."
echo "🎬 Compiling all maps..."
for map in "${maps[@]}"; do
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
done
echo "✓ Map compilation complete"
echo "✓ Map compilation complete"
}
}
@@ -119,7 +78,7 @@ main() {
@@ -119,7 +78,7 @@ main() {
compile_all_maps
compile_all_maps
echo ""
echo ""
- echo "✅ All done! BSP files ready in: $MAPS_DIR"
+ echo "✅ All done! BSP files ready in: $ROOT/game/openvibe.games/maps"
echo ""
echo ""
echo "Next steps:"
echo "Next steps:"
echo " 1. Copy BSP files to your SRCDS installation"
echo " 1. Copy BSP files to your SRCDS installation"
@@ -1,8 +1,47 @@
@@ -1,8 +1,47 @@
#!/usr/bin/env bash
#!/usr/bin/env bash
set -euo pipefail
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
tmux kill-session -t "$session" 2>/dev/null || true
done
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"
@@ -348,7 +348,7 @@ function hub() {
@@ -348,7 +348,7 @@ function hub() {
[pad.origin[0] - 128, pad.origin[1] - 128, 16],
[pad.origin[0] - 128, pad.origin[1] - 128, 16],
[pad.origin[0] + 128, pad.origin[1] + 128, 128],
[pad.origin[0] + 128, pad.origin[1] + 128, 128],
commandTarget,
commandTarget,
- `connect 127.0.0.1:${pad.port}`,
+ `ov_join ${pad.mode}`,
),
),
];
];
}),
}),
@@ -417,7 +417,7 @@ function minigame(mapName, modeName, returnX = 0) {
@@ -417,7 +417,7 @@ function minigame(mapName, modeName, returnX = 0) {
[returnX - 128, 580, 16],
[returnX - 128, 580, 16],
[returnX + 128, 836, 128],
[returnX + 128, 836, 128],
"cmd_return_hub",
"cmd_return_hub",
- "connect 127.0.0.1:27015",
+ "ov_join hub",
),
),
];
];
@@ -15,6 +15,7 @@ CFG="$5"
@@ -15,6 +15,7 @@ CFG="$5"
ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
SRCDS="${OPENVIBE_SRCDS:-$HOME/srcds/tf2}"
SRCDS="${OPENVIBE_SRCDS:-$HOME/srcds/tf2}"
MOD="$ROOT/game/openvibe.games"
MOD="$ROOT/game/openvibe.games"
+MAP_DELAY="${OPENVIBE_SRCDS_MAP_DELAY:-6}"
if [[ ! -x "$SRCDS/srcds_linux64" ]]; then
if [[ ! -x "$SRCDS/srcds_linux64" ]]; then
echo "Missing 64-bit SRCDS at $SRCDS/srcds_linux64" >&2
echo "Missing 64-bit SRCDS at $SRCDS/srcds_linux64" >&2
@@ -30,14 +31,35 @@ fi
@@ -30,14 +31,35 @@ fi
cd "$SRCDS"
cd "$SRCDS"
+printf '243750\n' > "$SRCDS/steam_appid.txt"
+
export LD_LIBRARY_PATH="$MOD/bin/linux64:.:$SRCDS/bin/linux64:$SRCDS/bin:${LD_LIBRARY_PATH:-}"
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"
+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
@@ -12,7 +12,7 @@ run_check() {
@@ -12,7 +12,7 @@ run_check() {
local log="/tmp/openvibe-srcds-$name.log"
local log="/tmp/openvibe-srcds-$name.log"
set +e
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=$?
local code=$?
set -e
set -e
@@ -28,7 +28,13 @@ run_check() {
@@ -28,7 +28,13 @@ run_check() {
exit 1
exit 1
fi
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"
echo "[srcds:$name] did not reach map load for $map"
tail -120 "$log"
tail -120 "$log"
exit 1
exit 1