openvibers/source.openvibe.games Commits
Next
Changes
nnew file mode 100755BBinary files /dev/null and b/.tmp/quickjs-smoke/smoke-quickjs differnnew file mode 100644@@@ -0,0 +1,18 @@++#include <stdio.h>++#include "quickjs.h"++++int main(void) {++ JSRuntime *rt = JS_NewRuntime();++ JSContext *ctx = JS_NewContext(rt);++++ JSValue value = JS_Eval(ctx, "1 + 2 + 3", 9, "<smoke>", JS_EVAL_TYPE_GLOBAL);++ int32_t out = 0;++ JS_ToInt32(ctx, &out, value);++++ JS_FreeValue(ctx, value);++ JS_FreeContext(ctx);++ JS_FreeRuntime(rt);++++ printf("quickjs result=%d\n", out);++ return out == 6 ? 0 : 1;++}@@@ -1,8 +1,4 @@--import { hook } from "./core/hook.js";--import { gamemode } from "./core/gamemode.js";----globalThis.hook = hook;--globalThis.gamemode = gamemode;++OV.log("bridge.js loaded");globalThis.console = {log: (...args) => OV.log(args.map(String).join(" ")),@@@ -14,16 +10,11 @@ globalThis.game = {mode: () => OV.getMode(),map: () => OV.getMapName(),time: () => OV.time(),-- log: (msg) => OV.log(String(msg)),broadcast: (msg) => OV.broadcast(String(msg)),-- runServerCommand: (cmd) => OV.serverCommand(String(cmd))++ serverCommand: (cmd) => OV.serverCommand(String(cmd))};globalThis.round = {-- state: () => OV.roundState(),-- setState: (state) => OV.setRoundState(String(state)),start: () => OV.fireHook("RoundStart"),end: () => OV.fireHook("RoundEnd")};----OV.log("[OpenVibe JS] bridge.js loaded");@@@ -1 +1,26 @@--console.log("Base server realm loaded");++const GM = {++ mode: "base",++ name: "OpenVibe Base",++++ Initialize() {++ OV.log("Base Initialize fired");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source.");++ },++++ PlayerSpawn(_ply) {},++++ PlayerDeath(_victim, _attacker) {},++++ PlayerDisconnected(_ply) {},++++ PlayerSay(_ply, _text) {++ return undefined;++ },++++ Think() {}++};++++gamemode.set(GM);@@@ -1,36 +1,23 @@--let roundState = "waiting";--let roundEndsAt = 0;----export const GM = {++const GM = {mode: "deathrun",name: "OpenVibe Deathrun",Initialize() {-- roundState = "waiting";++ OV.log("Deathrun Initialize fired");},PlayerInitialSpawn(ply) {ply.chat("Deathrun: survive the traps and reach the finish.");},-- RoundStart() {-- roundState = "running";-- roundEndsAt = game.time() + 420;-- game.broadcast("Deathrun round started.");-- },---- PlayerFinish(ply) {-- ply.chat("Finished Deathrun!");-- OV.reward(ply, 50, 100, "deathrun_finish");-- return true;-- },---- Think() {-- if (roundState === "running" && game.time() >= roundEndsAt) {-- roundState = "ended";-- game.broadcast("Deathrun time expired.");-- OV.endMatch("time_expired");++ PlayerSay(ply, text) {++ if (text === "!finish") {++ ply.chat("Deathrun finish test.");++ OV.reward(ply, 50, 100, "deathrun_finish");++ return false;}++++ return undefined;}};@@@ -1,16 +1,11 @@--let state = "build";--let stateEndsAt = 0;++const allowed = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);--const allowedParts = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);----export const GM = {++const GM = {mode: "fortwars",name: "OpenVibe Fort Wars",Initialize() {-- state = "build";-- stateEndsAt = game.time() + 180;-- game.broadcast("Fort Wars build phase started.");++ OV.log("Fort Wars Initialize fired");},PlayerInitialSpawn(ply) {@@@ -20,33 +15,14 @@ export const GM = {PlayerSay(ply, text) {if (!text.startsWith("!build ")) return undefined;-- if (state !== "build") {-- ply.chat("Build phase is over.");-- return false;-- }---- const part = text.slice("!build ".length).trim();-- if (!allowedParts.has(part)) {++ const part = text.slice(7).trim();++ if (!allowed.has(part)) {ply.chat("Allowed: crate, barrel, pallet, fence, sheet");return false;}ply.runCommand(`ov_fortwars_spawn ${part}`);return false;-- },---- Think() {-- if (state === "build" && game.time() >= stateEndsAt) {-- state = "fight";-- stateEndsAt = game.time() + 420;-- game.broadcast("Fort Wars fight phase started.");-- }---- if (state === "fight" && game.time() >= stateEndsAt) {-- state = "ended";-- game.broadcast("Fort Wars ended.");-- OV.endMatch("time_expired");-- }}};@@@ -1,27 +1,44 @@--export const GM = {++const GM = {mode: "hub",name: "OpenVibe Hub",Initialize() {-- game.broadcast("OpenVibe Hub online");++ OV.log("Hub Initialize fired");++ },++++ MapInitialize(mapName) {++ OV.log(`Map initialized: ${mapName}`);},PlayerInitialSpawn(ply) {-- ply.chat("Welcome to the OpenVibe Hub");-- ply.chat("Use the portals to join Prop Hunt, Deathrun, Fort Wars, or Traitor Town.");++ ply.chat("Welcome to OpenVibe: Source JS runtime.");++ OV.broadcast(`${ply.name()} joined the hub.`);++ },++++ PlayerSpawn(ply) {++ ply.chat("PlayerSpawn hook fired.");},-- HubPortalUse(ply, mode) {-- ply.chat(`Sending you to ${mode}...`);-- ply.runCommand(`ov_join ${mode}`);-- return true;++ PlayerSay(ply, text) {++ if (text === "!js") {++ ply.chat("JavaScript hooks are working.");++ return false;++ }++++ if (text === "!hp") {++ ply.chat(`Health: ${ply.health()}`);++ return false;++ }++++ if (text === "!players") {++ ply.chat(`Players online: ${OV.players().length}`);++ return false;++ }++++ return undefined;},-- ShopNpcUse(ply, category) {-- ply.chat(`Opening ${category} shop...`);-- ply.runCommand(`ov_open_url https://openvibe.games/me/inventory`);-- return true;-- }++ Think() {}};gamemode.set(GM);@@@ -1,31 +1,21 @@--let state = "waiting";--let roundEndsAt = 0;----const propChoices = ["can", "crate", "barrel", "chair", "bucket"];++const props = ["crate", "barrel", "chair", "bucket"];function randomProp() {-- return propChoices[Math.floor(Math.random() * propChoices.length)];++ return props[Math.floor(Math.random() * props.length)];}--export const GM = {++const GM = {mode: "prophunt",name: "OpenVibe Prop Hunt",Initialize() {-- state = "waiting";-- console.log("Prop Hunt initialized");++ OV.log("Prop Hunt Initialize fired");},PlayerInitialSpawn(ply) {ply.chat("Prop Hunt: hide as props or hunt them down.");},-- PlayerSpawn(ply) {-- if (state === "hide") {-- ply.runCommand(`ov_prophunt_disguise ${randomProp()}`);-- }-- },--PlayerSay(ply, text) {if (text === "!prop") {ply.runCommand(`ov_prophunt_disguise ${randomProp()}`);@@@ -33,27 +23,14 @@ export const GM = {}if (text.startsWith("!prop ")) {-- const choice = text.slice("!prop ".length).trim();-- ply.runCommand(`ov_prophunt_disguise ${choice}`);++ ply.runCommand(`ov_prophunt_disguise ${text.slice(6).trim()}`);return false;}return undefined;},-- RoundStart() {-- state = "hide";-- roundEndsAt = game.time() + 300;-- game.broadcast("Prop Hunt round started. Props hide. Hunters seek.");-- },---- Think() {-- if (state === "hide" && game.time() >= roundEndsAt) {-- state = "ended";-- game.broadcast("Props win!");-- OV.endMatch("props_win");-- }-- }++ Think() {}};gamemode.set(GM);@@@ -1,49 +1,29 @@--let state = "waiting";--let roles = new Map();++const roles = new Map();--function assignRoles(players) {-- roles.clear();---- const shuffled = [...players].sort(() => Math.random() - 0.5);-- const traitorCount = Math.max(1, Math.floor(shuffled.length / 4));---- for (let i = 0; i < shuffled.length; i++) {-- const role = i < traitorCount ? "traitor" : "innocent";-- roles.set(shuffled[i].steamId(), role);-- shuffled[i].chat(`Your role: ${role}`);-- }--}----export const GM = {++const GM = {mode: "traitortown",name: "OpenVibe Traitor Town",Initialize() {-- state = "waiting";++ OV.log("Traitor Town Initialize fired");},PlayerInitialSpawn(ply) {ply.chat("Traitor Town: find the traitors before they find you.");-- },---- RoundStart() {-- state = "running";-- assignRoles(OV.players());-- game.broadcast("Traitor Town round started.");-- },---- PlayerDeath(victim, attacker) {-- const role = roles.get(victim.steamId()) || "unknown";-- game.broadcast(`${victim.name()} died. They were ${role}.`);++ roles.set(ply.userId(), "innocent");},PlayerSay(ply, text) {if (text === "!role") {-- ply.chat(`Your role: ${roles.get(ply.steamId()) || "none"}`);++ ply.chat(`Your role: ${roles.get(ply.userId()) || "none"}`);return false;}return undefined;++ },++++ PlayerDeath(victim, attacker) {++ OV.broadcast(`${victim.name()} died.`);}};nnew file mode 100644@@@ -0,0 +1,188 @@++#include "cbase.h"++#include "hl2mp_player.h"++#include "openvibe_js_server.h"++#include "openvibe/ov_js_runtime.h"++#include "openvibe/ov_js_player.h"++++#include "tier0/memdbgon.h"++++static ConVar ov_mode(++ "ov_mode",++ "hub",++ FCVAR_GAMEDLL,++ "OpenVibe mode: hub, prophunt, deathrun, fortwars, traitortown." );++++static ConVar ov_js_enabled(++ "ov_js_enabled",++ "1",++ FCVAR_GAMEDLL,++ "Enable OpenVibe JavaScript runtime." );++++static COpenVibeJSRuntime g_OVServerJS;++static bool g_OVServerJSStarted = false;++++static bool OpenVibeJS_IsRunning()++{++ return ov_js_enabled.GetBool() && g_OVServerJS.Context() != nullptr;++}++++static void OpenVibeJS_EnsureStarted()++{++ if (g_OVServerJSStarted)++ return;++++ g_OVServerJSStarted = true;++++ if (!ov_js_enabled.GetBool())++ {++ Msg("[OV JS] disabled by ov_js_enabled=0\n");++ return;++ }++++ if (g_OVServerJS.Init(true, ov_mode.GetString()))++ {++ Msg("[OV JS] server runtime initialized for mode '%s'\n", ov_mode.GetString());++ g_OVServerJS.CallHookVoid("Initialize");++++ JSContext *ctx = g_OVServerJS.Context();++ JSValue mapName = JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");++ JSValueConst argv[] = { mapName };++ g_OVServerJS.CallHookVoid("MapInitialize", 1, argv);++ JS_FreeValue(ctx, mapName);++ }++ else++ {++ Warning("[OV JS] server runtime failed to initialize\n");++ }++}++++void OpenVibeJS_ServerInit()++{++ OpenVibeJS_EnsureStarted();++}++++void OpenVibeJS_ServerShutdown()++{++ if (OpenVibeJS_IsRunning())++ g_OVServerJS.CallHookVoid("Shutdown");++++ g_OVServerJS.Shutdown();++ g_OVServerJSStarted = false;++}++++void OpenVibeJS_ServerThink()++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning())++ return;++++ g_OVServerJS.CallHookVoid("Think");++}++++void OpenVibeJS_Server_PlayerInitialSpawn(CHL2MP_Player *player)++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning() || !player)++ return;++++ JSContext *ctx = g_OVServerJS.Context();++ JSValue ply = OVJS_NewPlayer(ctx, player);++++ JSValueConst argv[] = { ply };++ g_OVServerJS.CallHookVoid("PlayerInitialSpawn", 1, argv);++++ JS_FreeValue(ctx, ply);++}++++void OpenVibeJS_Server_PlayerSpawn(CHL2MP_Player *player)++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning() || !player)++ return;++++ JSContext *ctx = g_OVServerJS.Context();++ JSValue ply = OVJS_NewPlayer(ctx, player);++++ JSValueConst argv[] = { ply };++ g_OVServerJS.CallHookVoid("PlayerSpawn", 1, argv);++++ JS_FreeValue(ctx, ply);++}++++void OpenVibeJS_Server_PlayerDeath(CHL2MP_Player *victim, CBaseEntity *attacker, CBaseEntity *inflictor)++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning() || !victim)++ return;++++ JSContext *ctx = g_OVServerJS.Context();++++ JSValue jsVictim = OVJS_NewPlayer(ctx, victim);++ JSValue jsAttacker = JS_NULL;++++ if (attacker && attacker->IsPlayer())++ jsAttacker = OVJS_NewPlayer(ctx, ToHL2MPPlayer(static_cast<CBasePlayer *>(attacker)));++++ JSValueConst argv[] = { jsVictim, jsAttacker };++ g_OVServerJS.CallHookVoid("PlayerDeath", 2, argv);++++ JS_FreeValue(ctx, jsVictim);++ JS_FreeValue(ctx, jsAttacker);++}++++void OpenVibeJS_Server_PlayerDisconnected(CHL2MP_Player *player)++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning() || !player)++ return;++++ JSContext *ctx = g_OVServerJS.Context();++ JSValue ply = OVJS_NewPlayer(ctx, player);++++ JSValueConst argv[] = { ply };++ g_OVServerJS.CallHookVoid("PlayerDisconnected", 1, argv);++++ JS_FreeValue(ctx, ply);++}++++bool OpenVibeJS_Server_PlayerSay(CHL2MP_Player *player, const char *text)++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning() || !player || !text)++ return false;++++ JSContext *ctx = g_OVServerJS.Context();++++ JSValue ply = OVJS_NewPlayer(ctx, player);++ JSValue msg = JS_NewString(ctx, text);++++ JSValueConst argv[] = { ply, msg };++++ bool value = false;++ bool returned = g_OVServerJS.CallHookBool("PlayerSay", &value, 2, argv);++++ JS_FreeValue(ctx, ply);++ JS_FreeValue(ctx, msg);++++ return returned && value == false;++}++++static void OV_JSReload_f()++{++ OpenVibeJS_ServerShutdown();++ OpenVibeJS_ServerInit();++ Msg("[OV JS] reloaded\n");++}++++static ConCommand ov_js_reload(++ "ov_js_reload",++ OV_JSReload_f,++ "Reload OpenVibe JavaScript runtime.",++ FCVAR_GAMEDLL++);nnew file mode 100644@@@ -0,0 +1,15 @@++#pragma once++++class CHL2MP_Player;++class CBasePlayer;++class CBaseEntity;++++void OpenVibeJS_ServerInit();++void OpenVibeJS_ServerShutdown();++void OpenVibeJS_ServerThink();++++void OpenVibeJS_Server_PlayerInitialSpawn(CHL2MP_Player *player);++void OpenVibeJS_Server_PlayerSpawn(CHL2MP_Player *player);++void OpenVibeJS_Server_PlayerDeath(CHL2MP_Player *victim, CBaseEntity *attacker, CBaseEntity *inflictor);++void OpenVibeJS_Server_PlayerDisconnected(CHL2MP_Player *player);++bool OpenVibeJS_Server_PlayerSay(CHL2MP_Player *player, const char *text);@@@ -8,6 +8,7 @@#include "cbase.h"#include "hl2mp_player.h"++#include "openvibe_js_server.h"#include "player.h"#include "props.h"#include "util.h"@@@ -259,6 +260,8 @@ void OpenVibe_OnClientActive( CHL2MP_Player *pPlayer ){engine->ServerCommand( UTIL_VarArgs( "kickid %d \"OpenVibe join token required\"\n", pPlayer->GetUserID() ) );}++++ OpenVibeJS_Server_PlayerInitialSpawn( pPlayer );}static void OV_PropHuntDisguise_f( const CCommand &args )@@@ -789,17 +792,22 @@ static COpenVibeJsBridge g_OpenVibeJsBridge;void OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer ){-- g_OpenVibeJsBridge.OnPlayerLeft( pPlayer );++++ CHL2MP_Player *pHL2MP = ToHL2MPPlayer( pPlayer );++ if ( pHL2MP )++ OpenVibeJS_Server_PlayerDisconnected( pHL2MP );}void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller ){-- g_OpenVibeJsBridge.OnPlayerDeath( pPlayer, pKiller );++++ OpenVibeJS_Server_PlayerDeath( pPlayer, pKiller, NULL );}void OpenVibe_OnFrame(){-- g_OpenVibeJsBridge.Update();++++ OpenVibeJS_ServerThink();}static void OV_JsCmd_f( const CCommand &args )nnew file mode 100644@@@ -0,0 +1,819 @@++// 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"++++// Standard library and socket includes for JS bridge++#include <thread>++#include <mutex>++#include <queue>++#include <string>++#include <condition_variable>++#include <sys/socket.h>++#include <sys/un.h>++#include <unistd.h>++#include <fcntl.h>++#include <sstream>++#include <sys/select.h>++#include <sys/time.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 );++++// =================================══════════════════════════════════════════++// OpenVibe.JS - Scripting Engine C++ Bridge++// =================================══════════════════════════════════════════++++class COpenVibeJsBridge++{++private:++ std::thread m_workerThread;++ std::mutex m_outgoingMutex;++ std::queue<std::string> m_outgoingQueue;++ std::mutex m_incomingMutex;++ std::queue<std::string> m_incomingQueue;++ bool m_bRunning;++ bool m_bConnected;++ int m_socketFd;++ std::string m_socketPath;++ std::string m_incomingBuffer;++++ void ThreadFunc()++ {++ while ( m_bRunning )++ {++ if ( !m_bConnected )++ {++ m_socketFd = socket( AF_UNIX, SOCK_STREAM, 0 );++ if ( m_socketFd >= 0 )++ {++ struct sockaddr_un addr;++ memset( &addr, 0, sizeof( addr ) );++ addr.sun_family = AF_UNIX;++ Q_strncpy( addr.sun_path, m_socketPath.c_str(), sizeof( addr.sun_path ) );++++ if ( connect( m_socketFd, (struct sockaddr*)&addr, sizeof( addr ) ) == 0 )++ {++ m_bConnected = true;++ Msg( "[OV JS] Connected to Node.js scripting bridge at %s\n", m_socketPath.c_str() );++ }++ else++ {++ close( m_socketFd );++ m_socketFd = -1;++ SpawnNodeJs();++ std::this_thread::sleep_for( std::chrono::milliseconds( 2000 ) );++ continue;++ }++ }++ }++++ if ( m_bConnected )++ {++ fd_set readFds;++ fd_set writeFds;++ FD_ZERO( &readFds );++ FD_ZERO( &writeFds );++ FD_SET( m_socketFd, &readFds );++++ bool bHasWrite = false;++ {++ std::lock_guard<std::mutex> lock( m_outgoingMutex );++ bHasWrite = !m_outgoingQueue.empty();++ }++ if ( bHasWrite )++ {++ FD_SET( m_socketFd, &writeFds );++ }++++ struct timeval tv;++ tv.tv_sec = 0;++ tv.tv_usec = 50000;++++ int activity = select( m_socketFd + 1, &readFds, &writeFds, NULL, &tv );++ if ( activity < 0 )++ {++ Disconnect();++ continue;++ }++++ if ( FD_ISSET( m_socketFd, &readFds ) )++ {++ char buffer[4096];++ int bytesRead = recv( m_socketFd, buffer, sizeof( buffer ) - 1, 0 );++ if ( bytesRead <= 0 )++ {++ Disconnect();++ continue;++ }++ buffer[bytesRead] = '\0';++ ProcessIncomingBuffer( buffer );++ }++++ if ( bHasWrite && FD_ISSET( m_socketFd, &writeFds ) )++ {++ std::string msg;++ {++ std::lock_guard<std::mutex> lock( m_outgoingMutex );++ if ( !m_outgoingQueue.empty() )++ {++ msg = m_outgoingQueue.front();++ m_outgoingQueue.pop();++ }++ }++ if ( !msg.empty() )++ {++ int sent = send( m_socketFd, msg.c_str(), msg.length(), 0 );++ if ( sent < 0 )++ {++ Disconnect();++ continue;++ }++ }++ }++ }++ }++ }++++ void SpawnNodeJs()++ {++ static float lastSpawnTime = 0.0f;++ float curTime = gpGlobals ? gpGlobals->realtime : 0.0f;++ if ( curTime > 0.0f && curTime - lastSpawnTime < 10.0f )++ {++ return;++ }++ lastSpawnTime = curTime;++++ Msg( "[OV JS] Starting Node.js OpenVibe.JS runtime...\n" );++ char cmd[1024];++ const char *root = getenv( "OPENVIBE_ROOT" );++ if ( !root )++ {++ root = "/home/workstation/src/openvibe-source";++ }++ Q_snprintf( cmd, sizeof( cmd ), "node %s/engine/openvibe-js-runtime/bridge.js %s > /tmp/openvibe-js-%s.log 2>&1 &",++ root, m_socketPath.c_str(), ov_server_id.GetString() );++ int ret = system( cmd );++ (void)ret;++ }++++ void Disconnect()++ {++ if ( m_socketFd >= 0 )++ {++ close( m_socketFd );++ m_socketFd = -1;++ }++ m_bConnected = false;++ Msg( "[OV JS] Disconnected from Node.js scripting bridge.\n" );++ }++++ void ProcessIncomingBuffer( const char *data )++ {++ m_incomingBuffer += data;++ size_t pos;++ while ( ( pos = m_incomingBuffer.find( '\n' ) ) != std::string::npos )++ {++ std::string line = m_incomingBuffer.substr( 0, pos );++ m_incomingBuffer.erase( 0, pos + 1 );++ if ( !line.empty() )++ {++ std::lock_guard<std::mutex> lock( m_incomingMutex );++ m_incomingQueue.push( line );++ }++ }++ }++++public:++ COpenVibeJsBridge() :++ m_bRunning( false ),++ m_bConnected( false ),++ m_socketFd( -1 )++ {++ }++++ ~COpenVibeJsBridge()++ {++ Stop();++ }++++ void Start()++ {++ if ( m_bRunning ) return;++++ char szSocket[256];++ Q_snprintf( szSocket, sizeof( szSocket ), "/tmp/openvibe-js-%s.sock", ov_server_id.GetString() );++ m_socketPath = szSocket;++++ m_bRunning = true;++ m_workerThread = std::thread( &COpenVibeJsBridge::ThreadFunc, this );++ Msg( "[OV JS] Scripting bridge service thread started. socket=%s\n", m_socketPath.c_str() );++ }++++ void Stop()++ {++ m_bRunning = false;++ if ( m_workerThread.joinable() )++ {++ m_workerThread.join();++ }++ Disconnect();++ }++++ void SendMessage( const std::string &msg )++ {++ if ( !m_bConnected ) return;++ std::lock_guard<std::mutex> lock( m_outgoingMutex );++ m_outgoingQueue.push( msg + "\n" );++ }++++ void OnPlayerJoined( CBasePlayer *pPlayer )++ {++ if ( !pPlayer ) return;++++ char szSteamID[32];++ CSteamID steamID;++ if ( pPlayer->GetSteamID( &steamID ) )++ {++ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );++ }++ else++ {++ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );++ }++++ std::stringstream ss;++ ss << "{\"type\":\"player_join\",\"data\":{"++ << "\"steamId\":\"" << szSteamID << "\","++ << "\"name\":\"" << pPlayer->GetPlayerName() << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void OnPlayerLeft( CBasePlayer *pPlayer )++ {++ if ( !pPlayer ) return;++++ char szSteamID[32];++ CSteamID steamID;++ if ( pPlayer->GetSteamID( &steamID ) )++ {++ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );++ }++ else++ {++ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );++ }++++ std::stringstream ss;++ ss << "{\"type\":\"player_leave\",\"data\":{"++ << "\"steamId\":\"" << szSteamID << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void OnPlayerDeath( CBasePlayer *pPlayer, CBaseEntity *pKiller )++ {++ if ( !pPlayer ) return;++++ char szDeadSteamID[32];++ CSteamID deadSteamID;++ if ( pPlayer->GetSteamID( &deadSteamID ) )++ {++ Q_snprintf( szDeadSteamID, sizeof( szDeadSteamID ), "%llu", deadSteamID.ConvertToUint64() );++ }++ else++ {++ Q_strncpy( szDeadSteamID, "0", sizeof( szDeadSteamID ) );++ }++++ char szKillerSteamID[32] = "0";++ if ( pKiller && pKiller->IsPlayer() )++ {++ CBasePlayer *pKillerPlayer = (CBasePlayer*)pKiller;++ CSteamID killerSteamID;++ if ( pKillerPlayer->GetSteamID( &killerSteamID ) )++ {++ Q_snprintf( szKillerSteamID, sizeof( szKillerSteamID ), "%llu", killerSteamID.ConvertToUint64() );++ }++ }++++ std::stringstream ss;++ ss << "{\"type\":\"player_death\",\"data\":{"++ << "\"deadSteamId\":\"" << szDeadSteamID << "\","++ << "\"killerSteamId\":\"" << szKillerSteamID << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void OnCommand( const char *cmd )++ {++ std::stringstream ss;++ ss << "{\"type\":\"command\",\"data\":{"++ << "\"cmd\":\"" << cmd << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void Update()++ {++ if ( !m_bRunning )++ {++ Start();++ }++++ static float lastTickTime = 0.0f;++ float curTime = gpGlobals->curtime;++ if ( curTime - lastTickTime >= 1.0f )++ {++ lastTickTime = curTime;++ SendMessage( "{\"type\":\"tick\",\"data\":{}}" );++ }++++ std::queue<std::string> localQueue;++ {++ std::lock_guard<std::mutex> lock( m_incomingMutex );++ while ( !m_incomingQueue.empty() )++ {++ localQueue.push( m_incomingQueue.front() );++ m_incomingQueue.pop();++ }++ }++++ while ( !localQueue.empty() )++ {++ std::string line = localQueue.front();++ localQueue.pop();++ ProcessMessage( line );++ }++ }++++private:++ void ProcessMessage( const std::string &line )++ {++ if ( line.find( "\"type\":\"broadcast\"" ) != std::string::npos )++ {++ size_t msgPos = line.find( "\"message\":\"" );++ if ( msgPos != std::string::npos )++ {++ msgPos += 11;++ size_t endPos = line.find( "\"", msgPos );++ if ( endPos != std::string::npos )++ {++ std::string msg = line.substr( msgPos, endPos - msgPos );++ UTIL_ClientPrintAll( HUD_PRINTTALK, "[OV JS] %s", msg.c_str() );++ }++ }++ }++ else if ( line.find( "\"type\":\"entity_spawn\"" ) != std::string::npos )++ {++ std::string type = "prop_physics_override";++ std::string model = "";++ Vector pos( 0, 0, 0 );++++ size_t tPos = line.find( "\"type\":\"" );++ if ( tPos != std::string::npos )++ {++ size_t dataPos = line.find( "\"data\":{" );++ if ( dataPos != std::string::npos )++ {++ size_t innerType = line.find( "\"type\":\"", dataPos );++ if ( innerType != std::string::npos )++ {++ innerType += 8;++ size_t end = line.find( "\"", innerType );++ if ( end != std::string::npos )++ {++ type = line.substr( innerType, end - innerType );++ }++ }++ size_t innerModel = line.find( "\"model\":\"", dataPos );++ if ( innerModel != std::string::npos )++ {++ innerModel += 9;++ size_t end = line.find( "\"", innerModel );++ if ( end != std::string::npos )++ {++ model = line.substr( innerModel, end - innerModel );++ }++ }++ size_t xPos = line.find( "\"x\":", dataPos );++ size_t yPos = line.find( "\"y\":", dataPos );++ size_t zPos = line.find( "\"z\":", dataPos );++ if ( xPos != std::string::npos && yPos != std::string::npos && zPos != std::string::npos )++ {++ pos.x = atof( line.c_str() + xPos + 4 );++ pos.y = atof( line.c_str() + yPos + 4 );++ pos.z = atof( line.c_str() + zPos + 4 );++ }++ }++ }++++ if ( !model.empty() )++ {++ CBaseEntity::PrecacheModel( model.c_str() );++ CBaseEntity *pEnt = CreateEntityByName( type.c_str() );++ if ( pEnt )++ {++ pEnt->KeyValue( "model", model.c_str() );++ pEnt->SetAbsOrigin( pos );++ DispatchSpawn( pEnt );++ pEnt->Activate();++ Msg( "[OV JS] Spawned entity %s with model %s at (%f, %f, %f)\n",++ type.c_str(), model.c_str(), pos.x, pos.y, pos.z );++ }++ }++ }++ }++};++++static COpenVibeJsBridge g_OpenVibeJsBridge;++++void OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer )++{++ g_OpenVibeJsBridge.OnPlayerLeft( pPlayer );++}++++void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller )++{++ g_OpenVibeJsBridge.OnPlayerDeath( pPlayer, pKiller );++}++++void OpenVibe_OnFrame()++{++ g_OpenVibeJsBridge.Update();++}++++static void OV_JsCmd_f( const CCommand &args )++{++ if ( args.ArgC() < 2 )++ {++ Msg( "Usage: ov_js_cmd <command_or_json>\n" );++ return;++ }++ g_OpenVibeJsBridge.OnCommand( args.ArgS() );++}++++static ConCommand ov_js_cmd(++ "ov_js_cmd",++ OV_JsCmd_f,++ "Send a command or JSON message to the OpenVibe.JS runtime.",++ FCVAR_GAMEDLL );nnew file mode 100644@@@ -0,0 +1,827 @@++// 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 "openvibe_js_server.h"++#include "player.h"++#include "props.h"++#include "util.h"++++#include "steam/steam_gameserver.h"++#include "steam/isteamhttp.h"++++// Standard library and socket includes for JS bridge++#include <thread>++#include <mutex>++#include <queue>++#include <string>++#include <condition_variable>++#include <sys/socket.h>++#include <sys/un.h>++#include <unistd.h>++#include <fcntl.h>++#include <sstream>++#include <sys/select.h>++#include <sys/time.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() ) );++ }++++ OpenVibeJS_Server_PlayerInitialSpawn( pPlayer );++}++++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 );++++// =================================══════════════════════════════════════════++// OpenVibe.JS - Scripting Engine C++ Bridge++// =================================══════════════════════════════════════════++++class COpenVibeJsBridge++{++private:++ std::thread m_workerThread;++ std::mutex m_outgoingMutex;++ std::queue<std::string> m_outgoingQueue;++ std::mutex m_incomingMutex;++ std::queue<std::string> m_incomingQueue;++ bool m_bRunning;++ bool m_bConnected;++ int m_socketFd;++ std::string m_socketPath;++ std::string m_incomingBuffer;++++ void ThreadFunc()++ {++ while ( m_bRunning )++ {++ if ( !m_bConnected )++ {++ m_socketFd = socket( AF_UNIX, SOCK_STREAM, 0 );++ if ( m_socketFd >= 0 )++ {++ struct sockaddr_un addr;++ memset( &addr, 0, sizeof( addr ) );++ addr.sun_family = AF_UNIX;++ Q_strncpy( addr.sun_path, m_socketPath.c_str(), sizeof( addr.sun_path ) );++++ if ( connect( m_socketFd, (struct sockaddr*)&addr, sizeof( addr ) ) == 0 )++ {++ m_bConnected = true;++ Msg( "[OV JS] Connected to Node.js scripting bridge at %s\n", m_socketPath.c_str() );++ }++ else++ {++ close( m_socketFd );++ m_socketFd = -1;++ SpawnNodeJs();++ std::this_thread::sleep_for( std::chrono::milliseconds( 2000 ) );++ continue;++ }++ }++ }++++ if ( m_bConnected )++ {++ fd_set readFds;++ fd_set writeFds;++ FD_ZERO( &readFds );++ FD_ZERO( &writeFds );++ FD_SET( m_socketFd, &readFds );++++ bool bHasWrite = false;++ {++ std::lock_guard<std::mutex> lock( m_outgoingMutex );++ bHasWrite = !m_outgoingQueue.empty();++ }++ if ( bHasWrite )++ {++ FD_SET( m_socketFd, &writeFds );++ }++++ struct timeval tv;++ tv.tv_sec = 0;++ tv.tv_usec = 50000;++++ int activity = select( m_socketFd + 1, &readFds, &writeFds, NULL, &tv );++ if ( activity < 0 )++ {++ Disconnect();++ continue;++ }++++ if ( FD_ISSET( m_socketFd, &readFds ) )++ {++ char buffer[4096];++ int bytesRead = recv( m_socketFd, buffer, sizeof( buffer ) - 1, 0 );++ if ( bytesRead <= 0 )++ {++ Disconnect();++ continue;++ }++ buffer[bytesRead] = '\0';++ ProcessIncomingBuffer( buffer );++ }++++ if ( bHasWrite && FD_ISSET( m_socketFd, &writeFds ) )++ {++ std::string msg;++ {++ std::lock_guard<std::mutex> lock( m_outgoingMutex );++ if ( !m_outgoingQueue.empty() )++ {++ msg = m_outgoingQueue.front();++ m_outgoingQueue.pop();++ }++ }++ if ( !msg.empty() )++ {++ int sent = send( m_socketFd, msg.c_str(), msg.length(), 0 );++ if ( sent < 0 )++ {++ Disconnect();++ continue;++ }++ }++ }++ }++ }++ }++++ void SpawnNodeJs()++ {++ static float lastSpawnTime = 0.0f;++ float curTime = gpGlobals ? gpGlobals->realtime : 0.0f;++ if ( curTime > 0.0f && curTime - lastSpawnTime < 10.0f )++ {++ return;++ }++ lastSpawnTime = curTime;++++ Msg( "[OV JS] Starting Node.js OpenVibe.JS runtime...\n" );++ char cmd[1024];++ const char *root = getenv( "OPENVIBE_ROOT" );++ if ( !root )++ {++ root = "/home/workstation/src/openvibe-source";++ }++ Q_snprintf( cmd, sizeof( cmd ), "node %s/engine/openvibe-js-runtime/bridge.js %s > /tmp/openvibe-js-%s.log 2>&1 &",++ root, m_socketPath.c_str(), ov_server_id.GetString() );++ int ret = system( cmd );++ (void)ret;++ }++++ void Disconnect()++ {++ if ( m_socketFd >= 0 )++ {++ close( m_socketFd );++ m_socketFd = -1;++ }++ m_bConnected = false;++ Msg( "[OV JS] Disconnected from Node.js scripting bridge.\n" );++ }++++ void ProcessIncomingBuffer( const char *data )++ {++ m_incomingBuffer += data;++ size_t pos;++ while ( ( pos = m_incomingBuffer.find( '\n' ) ) != std::string::npos )++ {++ std::string line = m_incomingBuffer.substr( 0, pos );++ m_incomingBuffer.erase( 0, pos + 1 );++ if ( !line.empty() )++ {++ std::lock_guard<std::mutex> lock( m_incomingMutex );++ m_incomingQueue.push( line );++ }++ }++ }++++public:++ COpenVibeJsBridge() :++ m_bRunning( false ),++ m_bConnected( false ),++ m_socketFd( -1 )++ {++ }++++ ~COpenVibeJsBridge()++ {++ Stop();++ }++++ void Start()++ {++ if ( m_bRunning ) return;++++ char szSocket[256];++ Q_snprintf( szSocket, sizeof( szSocket ), "/tmp/openvibe-js-%s.sock", ov_server_id.GetString() );++ m_socketPath = szSocket;++++ m_bRunning = true;++ m_workerThread = std::thread( &COpenVibeJsBridge::ThreadFunc, this );++ Msg( "[OV JS] Scripting bridge service thread started. socket=%s\n", m_socketPath.c_str() );++ }++++ void Stop()++ {++ m_bRunning = false;++ if ( m_workerThread.joinable() )++ {++ m_workerThread.join();++ }++ Disconnect();++ }++++ void SendMessage( const std::string &msg )++ {++ if ( !m_bConnected ) return;++ std::lock_guard<std::mutex> lock( m_outgoingMutex );++ m_outgoingQueue.push( msg + "\n" );++ }++++ void OnPlayerJoined( CBasePlayer *pPlayer )++ {++ if ( !pPlayer ) return;++++ char szSteamID[32];++ CSteamID steamID;++ if ( pPlayer->GetSteamID( &steamID ) )++ {++ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );++ }++ else++ {++ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );++ }++++ std::stringstream ss;++ ss << "{\"type\":\"player_join\",\"data\":{"++ << "\"steamId\":\"" << szSteamID << "\","++ << "\"name\":\"" << pPlayer->GetPlayerName() << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void OnPlayerLeft( CBasePlayer *pPlayer )++ {++ if ( !pPlayer ) return;++++ char szSteamID[32];++ CSteamID steamID;++ if ( pPlayer->GetSteamID( &steamID ) )++ {++ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );++ }++ else++ {++ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );++ }++++ std::stringstream ss;++ ss << "{\"type\":\"player_leave\",\"data\":{"++ << "\"steamId\":\"" << szSteamID << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void OnPlayerDeath( CBasePlayer *pPlayer, CBaseEntity *pKiller )++ {++ if ( !pPlayer ) return;++++ char szDeadSteamID[32];++ CSteamID deadSteamID;++ if ( pPlayer->GetSteamID( &deadSteamID ) )++ {++ Q_snprintf( szDeadSteamID, sizeof( szDeadSteamID ), "%llu", deadSteamID.ConvertToUint64() );++ }++ else++ {++ Q_strncpy( szDeadSteamID, "0", sizeof( szDeadSteamID ) );++ }++++ char szKillerSteamID[32] = "0";++ if ( pKiller && pKiller->IsPlayer() )++ {++ CBasePlayer *pKillerPlayer = (CBasePlayer*)pKiller;++ CSteamID killerSteamID;++ if ( pKillerPlayer->GetSteamID( &killerSteamID ) )++ {++ Q_snprintf( szKillerSteamID, sizeof( szKillerSteamID ), "%llu", killerSteamID.ConvertToUint64() );++ }++ }++++ std::stringstream ss;++ ss << "{\"type\":\"player_death\",\"data\":{"++ << "\"deadSteamId\":\"" << szDeadSteamID << "\","++ << "\"killerSteamId\":\"" << szKillerSteamID << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void OnCommand( const char *cmd )++ {++ std::stringstream ss;++ ss << "{\"type\":\"command\",\"data\":{"++ << "\"cmd\":\"" << cmd << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void Update()++ {++ if ( !m_bRunning )++ {++ Start();++ }++++ static float lastTickTime = 0.0f;++ float curTime = gpGlobals->curtime;++ if ( curTime - lastTickTime >= 1.0f )++ {++ lastTickTime = curTime;++ SendMessage( "{\"type\":\"tick\",\"data\":{}}" );++ }++++ std::queue<std::string> localQueue;++ {++ std::lock_guard<std::mutex> lock( m_incomingMutex );++ while ( !m_incomingQueue.empty() )++ {++ localQueue.push( m_incomingQueue.front() );++ m_incomingQueue.pop();++ }++ }++++ while ( !localQueue.empty() )++ {++ std::string line = localQueue.front();++ localQueue.pop();++ ProcessMessage( line );++ }++ }++++private:++ void ProcessMessage( const std::string &line )++ {++ if ( line.find( "\"type\":\"broadcast\"" ) != std::string::npos )++ {++ size_t msgPos = line.find( "\"message\":\"" );++ if ( msgPos != std::string::npos )++ {++ msgPos += 11;++ size_t endPos = line.find( "\"", msgPos );++ if ( endPos != std::string::npos )++ {++ std::string msg = line.substr( msgPos, endPos - msgPos );++ UTIL_ClientPrintAll( HUD_PRINTTALK, "[OV JS] %s", msg.c_str() );++ }++ }++ }++ else if ( line.find( "\"type\":\"entity_spawn\"" ) != std::string::npos )++ {++ std::string type = "prop_physics_override";++ std::string model = "";++ Vector pos( 0, 0, 0 );++++ size_t tPos = line.find( "\"type\":\"" );++ if ( tPos != std::string::npos )++ {++ size_t dataPos = line.find( "\"data\":{" );++ if ( dataPos != std::string::npos )++ {++ size_t innerType = line.find( "\"type\":\"", dataPos );++ if ( innerType != std::string::npos )++ {++ innerType += 8;++ size_t end = line.find( "\"", innerType );++ if ( end != std::string::npos )++ {++ type = line.substr( innerType, end - innerType );++ }++ }++ size_t innerModel = line.find( "\"model\":\"", dataPos );++ if ( innerModel != std::string::npos )++ {++ innerModel += 9;++ size_t end = line.find( "\"", innerModel );++ if ( end != std::string::npos )++ {++ model = line.substr( innerModel, end - innerModel );++ }++ }++ size_t xPos = line.find( "\"x\":", dataPos );++ size_t yPos = line.find( "\"y\":", dataPos );++ size_t zPos = line.find( "\"z\":", dataPos );++ if ( xPos != std::string::npos && yPos != std::string::npos && zPos != std::string::npos )++ {++ pos.x = atof( line.c_str() + xPos + 4 );++ pos.y = atof( line.c_str() + yPos + 4 );++ pos.z = atof( line.c_str() + zPos + 4 );++ }++ }++ }++++ if ( !model.empty() )++ {++ CBaseEntity::PrecacheModel( model.c_str() );++ CBaseEntity *pEnt = CreateEntityByName( type.c_str() );++ if ( pEnt )++ {++ pEnt->KeyValue( "model", model.c_str() );++ pEnt->SetAbsOrigin( pos );++ DispatchSpawn( pEnt );++ pEnt->Activate();++ Msg( "[OV JS] Spawned entity %s with model %s at (%f, %f, %f)\n",++ type.c_str(), model.c_str(), pos.x, pos.y, pos.z );++ }++ }++ }++ }++};++++static COpenVibeJsBridge g_OpenVibeJsBridge;++++void OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer )++{++++ CHL2MP_Player *pHL2MP = ToHL2MPPlayer( pPlayer );++ if ( pHL2MP )++ OpenVibeJS_Server_PlayerDisconnected( pHL2MP );++}++++void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller )++{++++ OpenVibeJS_Server_PlayerDeath( pPlayer, pKiller, NULL );++}++++void OpenVibe_OnFrame()++{++++ OpenVibeJS_ServerThink();++}++++static void OV_JsCmd_f( const CCommand &args )++{++ if ( args.ArgC() < 2 )++ {++ Msg( "Usage: ov_js_cmd <command_or_json>\n" );++ return;++ }++ g_OpenVibeJsBridge.OnCommand( args.ArgS() );++}++++static ConCommand ov_js_cmd(++ "ov_js_cmd",++ OV_JsCmd_f,++ "Send a command or JSON message to the OpenVibe.JS runtime.",++ FCVAR_GAMEDLL );nnew file mode 100644@@@ -0,0 +1,827 @@++// 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 "openvibe_js_server.h"++#include "player.h"++#include "props.h"++#include "util.h"++++#include "steam/steam_gameserver.h"++#include "steam/isteamhttp.h"++++// Standard library and socket includes for JS bridge++#include <thread>++#include <mutex>++#include <queue>++#include <string>++#include <condition_variable>++#include <sys/socket.h>++#include <sys/un.h>++#include <unistd.h>++#include <fcntl.h>++#include <sstream>++#include <sys/select.h>++#include <sys/time.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() ) );++ }++++ OpenVibeJS_Server_PlayerInitialSpawn( pPlayer );++}++++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 );++++// =================================══════════════════════════════════════════++// OpenVibe.JS - Scripting Engine C++ Bridge++// =================================══════════════════════════════════════════++++class COpenVibeJsBridge++{++private:++ std::thread m_workerThread;++ std::mutex m_outgoingMutex;++ std::queue<std::string> m_outgoingQueue;++ std::mutex m_incomingMutex;++ std::queue<std::string> m_incomingQueue;++ bool m_bRunning;++ bool m_bConnected;++ int m_socketFd;++ std::string m_socketPath;++ std::string m_incomingBuffer;++++ void ThreadFunc()++ {++ while ( m_bRunning )++ {++ if ( !m_bConnected )++ {++ m_socketFd = socket( AF_UNIX, SOCK_STREAM, 0 );++ if ( m_socketFd >= 0 )++ {++ struct sockaddr_un addr;++ memset( &addr, 0, sizeof( addr ) );++ addr.sun_family = AF_UNIX;++ Q_strncpy( addr.sun_path, m_socketPath.c_str(), sizeof( addr.sun_path ) );++++ if ( connect( m_socketFd, (struct sockaddr*)&addr, sizeof( addr ) ) == 0 )++ {++ m_bConnected = true;++ Msg( "[OV JS] Connected to Node.js scripting bridge at %s\n", m_socketPath.c_str() );++ }++ else++ {++ close( m_socketFd );++ m_socketFd = -1;++ SpawnNodeJs();++ std::this_thread::sleep_for( std::chrono::milliseconds( 2000 ) );++ continue;++ }++ }++ }++++ if ( m_bConnected )++ {++ fd_set readFds;++ fd_set writeFds;++ FD_ZERO( &readFds );++ FD_ZERO( &writeFds );++ FD_SET( m_socketFd, &readFds );++++ bool bHasWrite = false;++ {++ std::lock_guard<std::mutex> lock( m_outgoingMutex );++ bHasWrite = !m_outgoingQueue.empty();++ }++ if ( bHasWrite )++ {++ FD_SET( m_socketFd, &writeFds );++ }++++ struct timeval tv;++ tv.tv_sec = 0;++ tv.tv_usec = 50000;++++ int activity = select( m_socketFd + 1, &readFds, &writeFds, NULL, &tv );++ if ( activity < 0 )++ {++ Disconnect();++ continue;++ }++++ if ( FD_ISSET( m_socketFd, &readFds ) )++ {++ char buffer[4096];++ int bytesRead = recv( m_socketFd, buffer, sizeof( buffer ) - 1, 0 );++ if ( bytesRead <= 0 )++ {++ Disconnect();++ continue;++ }++ buffer[bytesRead] = '\0';++ ProcessIncomingBuffer( buffer );++ }++++ if ( bHasWrite && FD_ISSET( m_socketFd, &writeFds ) )++ {++ std::string msg;++ {++ std::lock_guard<std::mutex> lock( m_outgoingMutex );++ if ( !m_outgoingQueue.empty() )++ {++ msg = m_outgoingQueue.front();++ m_outgoingQueue.pop();++ }++ }++ if ( !msg.empty() )++ {++ int sent = send( m_socketFd, msg.c_str(), msg.length(), 0 );++ if ( sent < 0 )++ {++ Disconnect();++ continue;++ }++ }++ }++ }++ }++ }++++ void SpawnNodeJs()++ {++ static float lastSpawnTime = 0.0f;++ float curTime = gpGlobals ? gpGlobals->realtime : 0.0f;++ if ( curTime > 0.0f && curTime - lastSpawnTime < 10.0f )++ {++ return;++ }++ lastSpawnTime = curTime;++++ Msg( "[OV JS] Starting Node.js OpenVibe.JS runtime...\n" );++ char cmd[1024];++ const char *root = getenv( "OPENVIBE_ROOT" );++ if ( !root )++ {++ root = "/home/workstation/src/openvibe-source";++ }++ Q_snprintf( cmd, sizeof( cmd ), "node %s/engine/openvibe-js-runtime/bridge.js %s > /tmp/openvibe-js-%s.log 2>&1 &",++ root, m_socketPath.c_str(), ov_server_id.GetString() );++ int ret = system( cmd );++ (void)ret;++ }++++ void Disconnect()++ {++ if ( m_socketFd >= 0 )++ {++ close( m_socketFd );++ m_socketFd = -1;++ }++ m_bConnected = false;++ Msg( "[OV JS] Disconnected from Node.js scripting bridge.\n" );++ }++++ void ProcessIncomingBuffer( const char *data )++ {++ m_incomingBuffer += data;++ size_t pos;++ while ( ( pos = m_incomingBuffer.find( '\n' ) ) != std::string::npos )++ {++ std::string line = m_incomingBuffer.substr( 0, pos );++ m_incomingBuffer.erase( 0, pos + 1 );++ if ( !line.empty() )++ {++ std::lock_guard<std::mutex> lock( m_incomingMutex );++ m_incomingQueue.push( line );++ }++ }++ }++++public:++ COpenVibeJsBridge() :++ m_bRunning( false ),++ m_bConnected( false ),++ m_socketFd( -1 )++ {++ }++++ ~COpenVibeJsBridge()++ {++ Stop();++ }++++ void Start()++ {++ if ( m_bRunning ) return;++++ char szSocket[256];++ Q_snprintf( szSocket, sizeof( szSocket ), "/tmp/openvibe-js-%s.sock", ov_server_id.GetString() );++ m_socketPath = szSocket;++++ m_bRunning = true;++ m_workerThread = std::thread( &COpenVibeJsBridge::ThreadFunc, this );++ Msg( "[OV JS] Scripting bridge service thread started. socket=%s\n", m_socketPath.c_str() );++ }++++ void Stop()++ {++ m_bRunning = false;++ if ( m_workerThread.joinable() )++ {++ m_workerThread.join();++ }++ Disconnect();++ }++++ void SendMessage( const std::string &msg )++ {++ if ( !m_bConnected ) return;++ std::lock_guard<std::mutex> lock( m_outgoingMutex );++ m_outgoingQueue.push( msg + "\n" );++ }++++ void OnPlayerJoined( CBasePlayer *pPlayer )++ {++ if ( !pPlayer ) return;++++ char szSteamID[32];++ CSteamID steamID;++ if ( pPlayer->GetSteamID( &steamID ) )++ {++ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );++ }++ else++ {++ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );++ }++++ std::stringstream ss;++ ss << "{\"type\":\"player_join\",\"data\":{"++ << "\"steamId\":\"" << szSteamID << "\","++ << "\"name\":\"" << pPlayer->GetPlayerName() << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void OnPlayerLeft( CBasePlayer *pPlayer )++ {++ if ( !pPlayer ) return;++++ char szSteamID[32];++ CSteamID steamID;++ if ( pPlayer->GetSteamID( &steamID ) )++ {++ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );++ }++ else++ {++ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );++ }++++ std::stringstream ss;++ ss << "{\"type\":\"player_leave\",\"data\":{"++ << "\"steamId\":\"" << szSteamID << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void OnPlayerDeath( CBasePlayer *pPlayer, CBaseEntity *pKiller )++ {++ if ( !pPlayer ) return;++++ char szDeadSteamID[32];++ CSteamID deadSteamID;++ if ( pPlayer->GetSteamID( &deadSteamID ) )++ {++ Q_snprintf( szDeadSteamID, sizeof( szDeadSteamID ), "%llu", deadSteamID.ConvertToUint64() );++ }++ else++ {++ Q_strncpy( szDeadSteamID, "0", sizeof( szDeadSteamID ) );++ }++++ char szKillerSteamID[32] = "0";++ if ( pKiller && pKiller->IsPlayer() )++ {++ CBasePlayer *pKillerPlayer = (CBasePlayer*)pKiller;++ CSteamID killerSteamID;++ if ( pKillerPlayer->GetSteamID( &killerSteamID ) )++ {++ Q_snprintf( szKillerSteamID, sizeof( szKillerSteamID ), "%llu", killerSteamID.ConvertToUint64() );++ }++ }++++ std::stringstream ss;++ ss << "{\"type\":\"player_death\",\"data\":{"++ << "\"deadSteamId\":\"" << szDeadSteamID << "\","++ << "\"killerSteamId\":\"" << szKillerSteamID << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void OnCommand( const char *cmd )++ {++ std::stringstream ss;++ ss << "{\"type\":\"command\",\"data\":{"++ << "\"cmd\":\"" << cmd << "\""++ << "}}";++ SendMessage( ss.str() );++ }++++ void Update()++ {++ if ( !m_bRunning )++ {++ Start();++ }++++ static float lastTickTime = 0.0f;++ float curTime = gpGlobals->curtime;++ if ( curTime - lastTickTime >= 1.0f )++ {++ lastTickTime = curTime;++ SendMessage( "{\"type\":\"tick\",\"data\":{}}" );++ }++++ std::queue<std::string> localQueue;++ {++ std::lock_guard<std::mutex> lock( m_incomingMutex );++ while ( !m_incomingQueue.empty() )++ {++ localQueue.push( m_incomingQueue.front() );++ m_incomingQueue.pop();++ }++ }++++ while ( !localQueue.empty() )++ {++ std::string line = localQueue.front();++ localQueue.pop();++ ProcessMessage( line );++ }++ }++++private:++ void ProcessMessage( const std::string &line )++ {++ if ( line.find( "\"type\":\"broadcast\"" ) != std::string::npos )++ {++ size_t msgPos = line.find( "\"message\":\"" );++ if ( msgPos != std::string::npos )++ {++ msgPos += 11;++ size_t endPos = line.find( "\"", msgPos );++ if ( endPos != std::string::npos )++ {++ std::string msg = line.substr( msgPos, endPos - msgPos );++ UTIL_ClientPrintAll( HUD_PRINTTALK, "[OV JS] %s", msg.c_str() );++ }++ }++ }++ else if ( line.find( "\"type\":\"entity_spawn\"" ) != std::string::npos )++ {++ std::string type = "prop_physics_override";++ std::string model = "";++ Vector pos( 0, 0, 0 );++++ size_t tPos = line.find( "\"type\":\"" );++ if ( tPos != std::string::npos )++ {++ size_t dataPos = line.find( "\"data\":{" );++ if ( dataPos != std::string::npos )++ {++ size_t innerType = line.find( "\"type\":\"", dataPos );++ if ( innerType != std::string::npos )++ {++ innerType += 8;++ size_t end = line.find( "\"", innerType );++ if ( end != std::string::npos )++ {++ type = line.substr( innerType, end - innerType );++ }++ }++ size_t innerModel = line.find( "\"model\":\"", dataPos );++ if ( innerModel != std::string::npos )++ {++ innerModel += 9;++ size_t end = line.find( "\"", innerModel );++ if ( end != std::string::npos )++ {++ model = line.substr( innerModel, end - innerModel );++ }++ }++ size_t xPos = line.find( "\"x\":", dataPos );++ size_t yPos = line.find( "\"y\":", dataPos );++ size_t zPos = line.find( "\"z\":", dataPos );++ if ( xPos != std::string::npos && yPos != std::string::npos && zPos != std::string::npos )++ {++ pos.x = atof( line.c_str() + xPos + 4 );++ pos.y = atof( line.c_str() + yPos + 4 );++ pos.z = atof( line.c_str() + zPos + 4 );++ }++ }++ }++++ if ( !model.empty() )++ {++ CBaseEntity::PrecacheModel( model.c_str() );++ CBaseEntity *pEnt = CreateEntityByName( type.c_str() );++ if ( pEnt )++ {++ pEnt->KeyValue( "model", model.c_str() );++ pEnt->SetAbsOrigin( pos );++ DispatchSpawn( pEnt );++ pEnt->Activate();++ Msg( "[OV JS] Spawned entity %s with model %s at (%f, %f, %f)\n",++ type.c_str(), model.c_str(), pos.x, pos.y, pos.z );++ }++ }++ }++ }++};++++static COpenVibeJsBridge g_OpenVibeJsBridge;++++void OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer )++{++++ CHL2MP_Player *pHL2MP = ToHL2MPPlayer( pPlayer );++ if ( pHL2MP )++ OpenVibeJS_Server_PlayerDisconnected( pHL2MP );++}++++void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller )++{++++ OpenVibeJS_Server_PlayerDeath( pPlayer, pKiller, NULL );++}++++void OpenVibe_OnFrame()++{++++ OpenVibeJS_ServerThink();++}++++static void OV_JsCmd_f( const CCommand &args )++{++ if ( args.ArgC() < 2 )++ {++ Msg( "Usage: ov_js_cmd <command_or_json>\n" );++ return;++ }++ g_OpenVibeJsBridge.OnCommand( args.ArgS() );++}++++static ConCommand ov_js_cmd(++ "ov_js_cmd",++ OV_JsCmd_f,++ "Send a command or JSON message to the OpenVibe.JS runtime.",++ FCVAR_GAMEDLL );nnew file mode 100644@@@ -0,0 +1,170 @@++#include "cbase.h"++#include "ov_js_bindings.h"++#include "ov_js_runtime.h"++#include "ov_js_player.h"++#include "hl2mp_player.h"++#include "util.h"++++#include "tier0/memdbgon.h"++++static COpenVibeJSRuntime *g_OVRuntime = nullptr;++++static JSValue OVJS_log(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_UNDEFINED;++ const char *msg = JS_ToCString(ctx, argv[0]);++ if (msg) { Msg("[OV JS] %s\n", msg); JS_FreeCString(ctx, msg); }++ return JS_UNDEFINED;++}++++static JSValue OVJS_warn(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_UNDEFINED;++ const char *msg = JS_ToCString(ctx, argv[0]);++ if (msg) { Warning("[OV JS] %s\n", msg); JS_FreeCString(ctx, msg); }++ return JS_UNDEFINED;++}++++static JSValue OVJS_error(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_UNDEFINED;++ const char *msg = JS_ToCString(ctx, argv[0]);++ if (msg) { Warning("[OV JS ERROR] %s\n", msg); JS_FreeCString(ctx, msg); }++ return JS_UNDEFINED;++}++++static JSValue OVJS_getMode(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ return JS_NewString(ctx, g_OVRuntime ? g_OVRuntime->GetMode() : "unknown");++}++++static JSValue OVJS_getMapName(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ return JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");++}++++static JSValue OVJS_time(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ return JS_NewFloat64(ctx, gpGlobals ? gpGlobals->curtime : 0.0);++}++++static JSValue OVJS_players(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ JSValue arr = JS_NewArray(ctx);++ uint32 index = 0;++++ for (int i = 1; i <= gpGlobals->maxClients; ++i)++ {++ CBasePlayer *base = UTIL_PlayerByIndex(i);++ if (!base) continue;++++ CHL2MP_Player *player = ToHL2MPPlayer(base);++ if (!player) continue;++++ JS_SetPropertyUint32(ctx, arr, index++, OVJS_NewPlayer(ctx, player));++ }++++ return arr;++}++++static JSValue OVJS_playerByUserId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_NULL;++++ int32 userId = 0;++ JS_ToInt32(ctx, &userId, argv[0]);++++ CHL2MP_Player *player = OVJS_ResolvePlayerByUserId(userId);++ return player ? OVJS_NewPlayer(ctx, player) : JS_NULL;++}++++static JSValue OVJS_broadcast(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_UNDEFINED;++++ const char *msg = JS_ToCString(ctx, argv[0]);++ if (!msg) return JS_UNDEFINED;++++ UTIL_ClientPrintAll(HUD_PRINTTALK, msg);++++ JS_FreeCString(ctx, msg);++ return JS_UNDEFINED;++}++++static bool OVJS_IsAllowedServerCommand(const char *cmd)++{++ return cmd && (!Q_strnicmp(cmd, "ov_", 3) || !Q_strnicmp(cmd, "say ", 4));++}++++static JSValue OVJS_serverCommand(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_UNDEFINED;++++ const char *cmd = JS_ToCString(ctx, argv[0]);++ if (!cmd) return JS_UNDEFINED;++++ if (OVJS_IsAllowedServerCommand(cmd))++ engine->ServerCommand(UTIL_VarArgs("%s\n", cmd));++ else++ Warning("[OV JS] blocked serverCommand: %s\n", cmd);++++ JS_FreeCString(ctx, cmd);++ return JS_UNDEFINED;++}++++static JSValue OVJS_fireHook(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (!g_OVRuntime || argc < 1) return JS_UNDEFINED;++++ const char *hookName = JS_ToCString(ctx, argv[0]);++ if (!hookName) return JS_UNDEFINED;++++ JSValue result = g_OVRuntime->CallHookRaw(hookName, argc - 1, argv + 1);++++ JS_FreeCString(ctx, hookName);++ return result;++}++++static JSValue OVJS_reward(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ Warning("[OV JS] OV.reward stubbed; backend reward wiring comes next.\n");++ return JS_UNDEFINED;++}++++static JSValue OVJS_endMatch(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ Warning("[OV JS] OV.endMatch stubbed; backend match wiring comes next.\n");++ return JS_UNDEFINED;++}++++static const JSCFunctionListEntry OVFuncs[] =++{++ JS_CFUNC_DEF("log", 1, OVJS_log),++ JS_CFUNC_DEF("warn", 1, OVJS_warn),++ JS_CFUNC_DEF("error", 1, OVJS_error),++ JS_CFUNC_DEF("getMode", 0, OVJS_getMode),++ JS_CFUNC_DEF("getMapName", 0, OVJS_getMapName),++ JS_CFUNC_DEF("time", 0, OVJS_time),++ JS_CFUNC_DEF("players", 0, OVJS_players),++ JS_CFUNC_DEF("playerByUserId", 1, OVJS_playerByUserId),++ JS_CFUNC_DEF("broadcast", 1, OVJS_broadcast),++ JS_CFUNC_DEF("serverCommand", 1, OVJS_serverCommand),++ JS_CFUNC_DEF("fireHook", 1, OVJS_fireHook),++ JS_CFUNC_DEF("reward", 4, OVJS_reward),++ JS_CFUNC_DEF("endMatch", 1, OVJS_endMatch),++};++++void OVJS_RegisterNativeBindings(JSContext *ctx, COpenVibeJSRuntime *runtime)++{++ g_OVRuntime = runtime;++++ OVJS_RegisterPlayerClass(ctx);++++ JSValue global = JS_GetGlobalObject(ctx);++ JSValue ov = JS_NewObject(ctx);++++ JS_SetPropertyFunctionList(ctx, ov, OVFuncs, ARRAYSIZE(OVFuncs));++ JS_SetPropertyStr(ctx, global, "OV", ov);++++ JS_FreeValue(ctx, global);++}nnew file mode 100644@@@ -0,0 +1,7 @@++#pragma once++++#include "openvibe/third_party/quickjs/quickjs.h"++++class COpenVibeJSRuntime;++++void OVJS_RegisterNativeBindings(JSContext *ctx, COpenVibeJSRuntime *runtime);nnew file mode 100644@@@ -0,0 +1,210 @@++#include "cbase.h"++#include "hl2mp_player.h"++#include "player.h"++#include "util.h"++#include "ov_js_player.h"++++#include "tier0/memdbgon.h"++++struct OVJSPlayerHandle++{++ int userId;++ int entIndex;++};++++static JSClassID g_OVJSPlayerClassID;++++CHL2MP_Player *OVJS_ResolvePlayerByUserId(int userId)++{++ for (int i = 1; i <= gpGlobals->maxClients; ++i)++ {++ CBasePlayer *base = UTIL_PlayerByIndex(i);++ if (!base)++ continue;++++ CHL2MP_Player *player = ToHL2MPPlayer(base);++ if (!player)++ continue;++++ if (player->GetUserID() == userId)++ return player;++ }++++ return nullptr;++}++++static void OVJS_PlayerFinalizer(JSRuntime *rt, JSValue val)++{++ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque(val, g_OVJSPlayerClassID));++ delete handle;++}++++CHL2MP_Player *OVJS_GetPlayerFromThis(JSContext *ctx, JSValueConst thisVal)++{++ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));++ if (!handle)++ return nullptr;++++ CHL2MP_Player *player = OVJS_ResolvePlayerByUserId(handle->userId);++ if (!player)++ return nullptr;++++ if (player->entindex() != handle->entIndex)++ return nullptr;++++ return player;++}++++static JSValue OVJS_Player_userId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));++ return handle ? JS_NewInt32(ctx, handle->userId) : JS_UNDEFINED;++}++++static JSValue OVJS_Player_entIndex(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));++ return handle ? JS_NewInt32(ctx, handle->entIndex) : JS_UNDEFINED;++}++++static JSValue OVJS_Player_name(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ return JS_NewString(ctx, player ? player->GetPlayerName() : "");++}++++static JSValue OVJS_Player_steamId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ if (!player)++ return JS_NewString(ctx, "");++++ const char *netid = engine->GetPlayerNetworkIDString(player->edict());++ return JS_NewString(ctx, netid ? netid : "");++}++++static JSValue OVJS_Player_health(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ return JS_NewInt32(ctx, player ? player->GetHealth() : 0);++}++++static JSValue OVJS_Player_setHealth(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ if (!player || argc < 1)++ return JS_UNDEFINED;++++ int32 value = 0;++ JS_ToInt32(ctx, &value, argv[0]);++++ if (value < 0) value = 0;++ if (value > 1000) value = 1000;++++ player->SetHealth(value);++ return JS_UNDEFINED;++}++++static JSValue OVJS_Player_team(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ return JS_NewInt32(ctx, player ? player->GetTeamNumber() : 0);++}++++static JSValue OVJS_Player_setTeam(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ if (!player || argc < 1)++ return JS_UNDEFINED;++++ int32 team = 0;++ JS_ToInt32(ctx, &team, argv[0]);++++ if (team >= 0 && team <= 4)++ player->ChangeTeam(team);++++ return JS_UNDEFINED;++}++++static JSValue OVJS_Player_chat(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ if (!player || argc < 1)++ return JS_UNDEFINED;++++ const char *msg = JS_ToCString(ctx, argv[0]);++ if (!msg)++ return JS_UNDEFINED;++++ ClientPrint(player, HUD_PRINTTALK, msg);++++ JS_FreeCString(ctx, msg);++ return JS_UNDEFINED;++}++++static JSValue OVJS_Player_runCommand(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ if (!player || argc < 1)++ return JS_UNDEFINED;++++ const char *cmd = JS_ToCString(ctx, argv[0]);++ if (!cmd)++ return JS_UNDEFINED;++++ if (!Q_strnicmp(cmd, "ov_", 3))++ engine->ClientCommand(player->edict(), "%s\n", cmd);++ else++ Warning("[OV JS] blocked player command: %s\n", cmd);++++ JS_FreeCString(ctx, cmd);++ return JS_UNDEFINED;++}++++static const JSCFunctionListEntry OVJSPlayerProtoFuncs[] =++{++ JS_CFUNC_DEF("userId", 0, OVJS_Player_userId),++ JS_CFUNC_DEF("entIndex", 0, OVJS_Player_entIndex),++ JS_CFUNC_DEF("steamId", 0, OVJS_Player_steamId),++ JS_CFUNC_DEF("name", 0, OVJS_Player_name),++ JS_CFUNC_DEF("health", 0, OVJS_Player_health),++ JS_CFUNC_DEF("setHealth", 1, OVJS_Player_setHealth),++ JS_CFUNC_DEF("team", 0, OVJS_Player_team),++ JS_CFUNC_DEF("setTeam", 1, OVJS_Player_setTeam),++ JS_CFUNC_DEF("chat", 1, OVJS_Player_chat),++ JS_CFUNC_DEF("runCommand", 1, OVJS_Player_runCommand),++};++++void OVJS_RegisterPlayerClass(JSContext *ctx)++{++ JSRuntime *rt = JS_GetRuntime(ctx);++++ JS_NewClassID(&g_OVJSPlayerClassID);++++ JSClassDef cls = {};++ cls.class_name = "OpenVibePlayer";++ cls.finalizer = OVJS_PlayerFinalizer;++++ JS_NewClass(rt, g_OVJSPlayerClassID, &cls);++++ JSValue proto = JS_NewObject(ctx);++ JS_SetPropertyFunctionList(ctx, proto, OVJSPlayerProtoFuncs, ARRAYSIZE(OVJSPlayerProtoFuncs));++ JS_SetClassProto(ctx, g_OVJSPlayerClassID, proto);++}++++JSValue OVJS_NewPlayer(JSContext *ctx, CHL2MP_Player *player)++{++ if (!player)++ return JS_NULL;++++ JSValue obj = JS_NewObjectClass(ctx, g_OVJSPlayerClassID);++ if (JS_IsException(obj))++ return obj;++++ OVJSPlayerHandle *handle = new OVJSPlayerHandle();++ handle->userId = player->GetUserID();++ handle->entIndex = player->entindex();++++ JS_SetOpaque(obj, handle);++ return obj;++}nnew file mode 100644@@@ -0,0 +1,10 @@++#pragma once++++#include "openvibe/third_party/quickjs/quickjs.h"++++class CHL2MP_Player;++++void OVJS_RegisterPlayerClass(JSContext *ctx);++JSValue OVJS_NewPlayer(JSContext *ctx, CHL2MP_Player *player);++CHL2MP_Player *OVJS_ResolvePlayerByUserId(int userId);++CHL2MP_Player *OVJS_GetPlayerFromThis(JSContext *ctx, JSValueConst thisVal);@@@ -1,6 +1,7 @@#include "cbase.h"--#include "ov_js_runtime.h"#include "filesystem.h"++#include "ov_js_runtime.h"++#include "ov_js_bindings.h"#include "tier0/memdbgon.h"@@@ -16,13 +17,12 @@ COpenVibeJSRuntime::~COpenVibeJSRuntime()Shutdown();}--bool COpenVibeJSRuntime::Init(bool bServerRealm, const char *pszMode, const char *pszRootDir)++bool COpenVibeJSRuntime::Init(bool bServerRealm, const char *pszMode){Shutdown();m_bServerRealm = bServerRealm;-- Q_strncpy(m_szMode, pszMode ? pszMode : "hub", sizeof(m_szMode));-- Q_strncpy(m_szRootDir, pszRootDir ? pszRootDir : "js", sizeof(m_szRootDir));++ Q_strncpy(m_szMode, pszMode && pszMode[0] ? pszMode : "hub", sizeof(m_szMode));m_pRuntime = JS_NewRuntime();if (!m_pRuntime)@@@ -39,9 +39,15 @@ bool COpenVibeJSRuntime::Init(bool bServerRealm, const char *pszMode, const charreturn false;}-- // TODO: OV_RegisterNativeBindings(m_pCtx, this);++ OVJS_RegisterNativeBindings(m_pCtx, this);++++ if (!LoadCoreFiles())++ return false;++++ if (!LoadGamemode())++ return false;-- return LoadCoreFiles() && LoadGamemode();++ return true;}void COpenVibeJSRuntime::Shutdown()@@@ -69,7 +75,7 @@ bool COpenVibeJSRuntime::Eval(const char *pszCode, const char *pszFilename)pszCode,Q_strlen(pszCode),pszFilename ? pszFilename : "<openvibe>",-- JS_EVAL_TYPE_MODULE++ JS_EVAL_TYPE_GLOBAL);if (JS_IsException(result))@@@ -85,9 +91,6 @@ bool COpenVibeJSRuntime::Eval(const char *pszCode, const char *pszFilename)bool COpenVibeJSRuntime::LoadFile(const char *pszPath){-- if (!pszPath || !pszPath[0])-- return false;--FileHandle_t file = filesystem->Open(pszPath, "rb", "MOD");if (!file){@@@ -95,23 +98,23 @@ bool COpenVibeJSRuntime::LoadFile(const char *pszPath)return false;}-- const int size = filesystem->Size(file);-- if (size <= 0 || size > 512 * 1024)++ int size = filesystem->Size(file);++ if (size <= 0 || size > 1024 * 1024){filesystem->Close(file);Warning("[OV JS] Refusing script %s size=%d\n", pszPath, size);return false;}-- CUtlVector<char> buffer;-- buffer.SetSize(size + 1);---- filesystem->Read(buffer.Base(), size, file);++ char *buffer = new char[size + 1];++ filesystem->Read(buffer, size, file);filesystem->Close(file);--buffer[size] = '\0';-- return Eval(buffer.Base(), pszPath);++ bool ok = Eval(buffer, pszPath);++ delete[] buffer;++++ return ok;}bool COpenVibeJSRuntime::LoadCoreFiles()@@@ -130,26 +133,15 @@ bool COpenVibeJSRuntime::LoadCoreFiles()bool COpenVibeJSRuntime::LoadGamemode(){-- char path[256];---- Q_snprintf(path, sizeof(path), "js/gamemodes/base/shared.js");-- LoadFile(path);---- Q_snprintf(path, sizeof(path), "js/gamemodes/%s/shared.js", m_szMode);-- LoadFile(path);++ LoadFile("js/gamemodes/base/server.js");-- Q_snprintf(-- path,-- sizeof(path),-- "js/gamemodes/%s/%s.js",-- m_szMode,-- m_bServerRealm ? "server" : "client"-- );++ char path[256];++ Q_snprintf(path, sizeof(path), "js/gamemodes/%s/%s.js", m_szMode, m_bServerRealm ? "server" : "client");return LoadFile(path);}--JSValue COpenVibeJSRuntime::CallHook(const char *pszHookName, int argc, JSValueConst *argv)++JSValue COpenVibeJSRuntime::CallHookRaw(const char *pszHookName, int argc, JSValueConst *argv){if (!m_pCtx)return JS_UNDEFINED;@@@ -158,22 +150,27 @@ JSValue COpenVibeJSRuntime::CallHook(const char *pszHookName, int argc, JSValueCJSValue gamemode = JS_GetPropertyStr(m_pCtx, global, "gamemode");JSValue call = JS_GetPropertyStr(m_pCtx, gamemode, "call");-- JSValue hookName = JS_NewString(m_pCtx, pszHookName);++ if (!JS_IsFunction(m_pCtx, call))++ {++ JS_FreeValue(m_pCtx, call);++ JS_FreeValue(m_pCtx, gamemode);++ JS_FreeValue(m_pCtx, global);++ return JS_UNDEFINED;++ }++++ JSValue *callArgs = new JSValue[argc + 1];++ callArgs[0] = JS_NewString(m_pCtx, pszHookName);-- CUtlVector<JSValueConst> args;-- args.AddToTail(hookName);for (int i = 0; i < argc; ++i)-- args.AddToTail(argv[i]);++ callArgs[i + 1] = JS_DupValue(m_pCtx, argv[i]);-- JSValue result = JS_Call(-- m_pCtx,-- call,-- gamemode,-- args.Count(),-- args.Base()-- );++ JSValue result = JS_Call(m_pCtx, call, gamemode, argc + 1, callArgs);++++ for (int i = 0; i < argc + 1; ++i)++ JS_FreeValue(m_pCtx, callArgs[i]);++++ delete[] callArgs;-- JS_FreeValue(m_pCtx, hookName);JS_FreeValue(m_pCtx, call);JS_FreeValue(m_pCtx, gamemode);JS_FreeValue(m_pCtx, global);@@@ -188,6 +185,35 @@ JSValue COpenVibeJSRuntime::CallHook(const char *pszHookName, int argc, JSValueCreturn result;}++void COpenVibeJSRuntime::CallHookVoid(const char *pszHookName, int argc, JSValueConst *argv)++{++ if (!m_pCtx)++ return;++++ JSValue result = CallHookRaw(pszHookName, argc, argv);++ JS_FreeValue(m_pCtx, result);++}++++bool COpenVibeJSRuntime::CallHookBool(const char *pszHookName, bool *pOut, int argc, JSValueConst *argv)++{++ if (!m_pCtx)++ return false;++++ JSValue result = CallHookRaw(pszHookName, argc, argv);++++ if (JS_IsUndefined(result))++ {++ JS_FreeValue(m_pCtx, result);++ return false;++ }++++ if (pOut)++ *pOut = JS_ToBool(m_pCtx, result) != 0;++++ JS_FreeValue(m_pCtx, result);++ return true;++}++void COpenVibeJSRuntime::PrintException(const char *pszWhere){JSValue exception = JS_GetException(m_pCtx);@@@ -1,8 +1,6 @@#pragma once--#include "quickjs.h"----class CBasePlayer;++#include "openvibe/third_party/quickjs/quickjs.h"class COpenVibeJSRuntime{@@@ -10,15 +8,18 @@ public:COpenVibeJSRuntime();~COpenVibeJSRuntime();-- bool Init(bool bServerRealm, const char *pszMode, const char *pszRootDir);++ bool Init(bool bServerRealm, const char *pszMode);void Shutdown();bool LoadFile(const char *pszPath);bool Eval(const char *pszCode, const char *pszFilename);-- JSValue CallHook(const char *pszHookName, int argc = 0, JSValueConst *argv = nullptr);++ JSValue CallHookRaw(const char *pszHookName, int argc = 0, JSValueConst *argv = nullptr);++ void CallHookVoid(const char *pszHookName, int argc = 0, JSValueConst *argv = nullptr);++ bool CallHookBool(const char *pszHookName, bool *pOut, int argc = 0, JSValueConst *argv = nullptr);JSContext *Context() { return m_pCtx; }++ const char *GetMode() const { return m_szMode; }private:void PrintException(const char *pszWhere);@@@ -27,8 +28,6 @@ private:JSRuntime *m_pRuntime = nullptr;JSContext *m_pCtx = nullptr;--bool m_bServerRealm = false;char m_szMode[64]{};-- char m_szRootDir[512]{};};nnew file mode 100644@@@ -0,0 +1,13 @@++# QuickJS vendored for OpenVibe: Source++++Vendored QuickJS core files for OpenVibe's embedded JavaScript runtime.++++Excluded intentionally:++- qjs.c++- qjsc.c++- quickjs-libc.c++- quickjs-libc.h++++Reason:++OpenVibe embeds QuickJS as a sandboxed script VM. Community scripts should not++receive raw std/os/filesystem/process APIs.@@@ -17,6 +17,14 @@ copy_file() {echo "[openvibe-sdk] copied ${dst#$SDK/}"}++copy_tree() {++ local src="$1"++ local dst="$2"++ mkdir -p "$dst"++ rsync -a --delete "$src/" "$dst/"++ echo "[openvibe-sdk] copied tree ${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" \@@@ -24,20 +32,75 @@ copy_file "$ROOT/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp" \copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_server.cpp" \"$SDK/src/game/server/hl2mp/openvibe_server.cpp"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.h" \++ "$SDK/src/game/shared/openvibe/ov_js_runtime.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_runtime.cpp"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.h" \++ "$SDK/src/game/shared/openvibe/ov_js_bindings.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_bindings.cpp"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.h" \++ "$SDK/src/game/shared/openvibe/ov_js_player.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_player.cpp"++++copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.h" \++ "$SDK/src/game/server/hl2mp/openvibe_js_server.h"++copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp" \++ "$SDK/src/game/server/hl2mp/openvibe_js_server.cpp"++++copy_tree "$ROOT/sdk/openvibe/third_party/quickjs" \++ "$SDK/src/game/shared/openvibe/third_party/quickjs"++++QJS_C="$SDK/src/game/shared/openvibe/third_party/quickjs/quickjs.c"++if [[ -f "$QJS_C" ]] && ! grep -q 'OPENVIBE_QUICKJS_CONFIG' "$QJS_C"; then++ tmp="$(mktemp)"++ cat > "$tmp" <<'QCFG'++#ifndef OPENVIBE_QUICKJS_CONFIG++#define OPENVIBE_QUICKJS_CONFIG++#ifndef _GNU_SOURCE++#define _GNU_SOURCE++#endif++#ifndef CONFIG_VERSION++#define CONFIG_VERSION "openvibe"++#endif++#endif++++QCFG++ cat "$QJS_C" >> "$tmp"++ mv "$tmp" "$QJS_C"++fi++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"HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp"++GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"++HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.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"++perl -0pi -e '++ s/^.*hl2mp\\openvibe_server\.cpp.*\n//mg;++ s/^.*hl2mp\\openvibe_js_server\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_bindings\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_player\.cpp.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\quickjs\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libregexp\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libunicode\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\cutils\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\dtoa\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libbf\.c.*\n//mg;++ s/(\$File\s+"hl2mp\\hl2mp_player\.h"\n)/$1\t\t\t\$File\t"hl2mp\\openvibe_server.cpp"\n\t\t\t\$File\t"hl2mp\\openvibe_js_server.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_runtime.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_bindings.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_player.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\quickjs.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libregexp.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libunicode.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\cutils.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\dtoa.c"\n/s;++' "$SERVER_VPC"echo "[openvibe-sdk] patched server_hl2mp.vpc"if ! grep -q 'OpenVibe_OnClientActive' "$HL2MP_CLIENT"; thensed -i '/void Host_Say/a void OpenVibe_OnClientActive( CHL2MP_Player *pPlayer );' "$HL2MP_CLIENT"-- sed -i '/FinishClientPutInServer( pPlayer );/a \tOpenVibe_OnClientActive( pPlayer );' "$HL2MP_CLIENT"++ sed -i '/FinishClientPutInServer( pPlayer );/a \ OpenVibe_OnClientActive( pPlayer );' "$HL2MP_CLIENT"echo "[openvibe-sdk] patched hl2mp_client.cpp arrival hook"fi@@@ -51,9 +114,6 @@ fiperl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"--GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"--HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.cpp"--if [[ -f "$GAMEINTERFACE" ]]; thenif ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; thensed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"@@@ -74,6 +134,40 @@ if [[ -f "$HL2MP_PLAYER" ]]; thensed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"fi++++ if ! grep -q 'OpenVibeJS_Server_PlayerSpawn' "$HL2MP_PLAYER"; then++ sed -i '/CHL2MP_Player::Spawn/i void OpenVibeJS_Server_PlayerSpawn( CHL2MP_Player *pPlayer );' "$HL2MP_PLAYER"++ sed -i '0,/BaseClass::Spawn();/s//BaseClass::Spawn();\n\tOpenVibeJS_Server_PlayerSpawn( this );/' "$HL2MP_PLAYER"++ echo "[openvibe-sdk] patched hl2mp_player.cpp spawn hook"++ fi++fi++++if [[ -f "$HL2MP_CLIENT" ]] && ! grep -q 'OpenVibeJS_Server_PlayerSay' "$HL2MP_CLIENT"; then++ sed -i '/void Host_Say/a bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *pPlayer, const char *pszText );' "$HL2MP_CLIENT"++ python3 - "$HL2MP_CLIENT" <<'PY'++from pathlib import Path++import re, sys++++p = Path(sys.argv[1])++s = p.read_text()++++# Best-effort patch for common Source SDK 2013 HL2MP Host_Say shape:++# CHL2MP_Player *client = ...++# const char *p = args.ArgS();++if "OpenVibeJS_Server_PlayerSay( client," not in s:++ s2 = re.sub(++ r'(const char \*p\s*=\s*args\.ArgS\(\);\s*)',++ r'\1\n\tif ( client && OpenVibeJS_Server_PlayerSay( client, p ) )\n\t\treturn;\n',++ s,++ count=1,++ )++ if s2 != s:++ s = s2++ print("[openvibe-sdk] patched hl2mp_client.cpp PlayerSay hook")++ else:++ print("[openvibe-sdk] WARNING: could not auto-patch Host_Say PlayerSay body")++p.write_text(s)++PYfiecho "[openvibe-sdk] Source SDK OpenVibe patch applied"nnew file mode 100755@@@ -0,0 +1,79 @@++#!/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"++HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.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"++++if ! grep -q 'OpenVibe_OnClientModeInit' "$HL2MP_CLIENTMODE"; then++ sed -i '/#include "ienginevgui.h"/a void OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"++ sed -i '/BaseClass::Init();/a \ OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"++ echo "[openvibe-sdk] patched clientmode_hl2mpnormal.cpp OpenVibe menu hook"++fi++++perl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"++++GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"++HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.cpp"++++if [[ -f "$GAMEINTERFACE" ]]; then++ if ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; then++ sed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"++ sed -i '/VPROF( "CServerGameDLL::GameFrame" );/a #ifdef HL2MP\n\tOpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"++ echo "[openvibe-sdk] patched gameinterface.cpp frame hook"++ fi++++ if ! grep -q 'OpenVibe_OnClientDisconnect' "$GAMEINTERFACE"; then++ perl -pi -e 's/void CServerGameClients::ClientDisconnect/#ifdef HL2MP\nvoid OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer );\n#endif\nvoid CServerGameClients::ClientDisconnect/' "$GAMEINTERFACE"++ perl -0777 -pi -e 's/(void CServerGameClients::ClientDisconnect\( edict_t \*pEdict \)\s*\{\s*extern bool\s+g_fGameOver;\s*CBasePlayer \*player = \( CBasePlayer \* \)CBaseEntity::Instance\( pEdict \);)/$1\n#ifdef HL2MP\n\tif ( player ) { OpenVibe_OnClientDisconnect( player ); }\n#endif/g' "$GAMEINTERFACE"++ echo "[openvibe-sdk] patched gameinterface.cpp disconnect hook"++ fi++fi++++if [[ -f "$HL2MP_PLAYER" ]]; then++ if ! grep -q 'OpenVibe_OnPlayerDeath' "$HL2MP_PLAYER"; then++ sed -i '/CHL2MP_Player::Event_Killed/i void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller );' "$HL2MP_PLAYER"++ sed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"++ echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"++ fi++fi++++echo "[openvibe-sdk] Source SDK OpenVibe patch applied"nnew file mode 100755@@@ -0,0 +1,173 @@++#!/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_tree() {++ local src="$1"++ local dst="$2"++ mkdir -p "$dst"++ rsync -a --delete "$src/" "$dst/"++ echo "[openvibe-sdk] copied tree ${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"++++copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.h" \++ "$SDK/src/game/shared/openvibe/ov_js_runtime.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_runtime.cpp"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.h" \++ "$SDK/src/game/shared/openvibe/ov_js_bindings.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_bindings.cpp"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.h" \++ "$SDK/src/game/shared/openvibe/ov_js_player.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_player.cpp"++++copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.h" \++ "$SDK/src/game/server/hl2mp/openvibe_js_server.h"++copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp" \++ "$SDK/src/game/server/hl2mp/openvibe_js_server.cpp"++++copy_tree "$ROOT/sdk/openvibe/third_party/quickjs" \++ "$SDK/src/game/shared/openvibe/third_party/quickjs"++++QJS_C="$SDK/src/game/shared/openvibe/third_party/quickjs/quickjs.c"++if [[ -f "$QJS_C" ]] && ! grep -q 'OPENVIBE_QUICKJS_CONFIG' "$QJS_C"; then++ tmp="$(mktemp)"++ cat > "$tmp" <<'QCFG'++#ifndef OPENVIBE_QUICKJS_CONFIG++#define OPENVIBE_QUICKJS_CONFIG++#ifndef _GNU_SOURCE++#define _GNU_SOURCE++#endif++#ifndef CONFIG_VERSION++#define CONFIG_VERSION "openvibe"++#endif++#endif++++QCFG++ cat "$QJS_C" >> "$tmp"++ mv "$tmp" "$QJS_C"++fi++++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"++HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp"++GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"++HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.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/^.*hl2mp\\openvibe_js_server\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_bindings\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_player\.cpp.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\quickjs\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libregexp\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libunicode\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\cutils\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\dtoa\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libbf\.c.*\n//mg;++ s/(\$File\s+"hl2mp\\hl2mp_player\.h"\n)/$1\t\t\t\$File\t"hl2mp\\openvibe_server.cpp"\n\t\t\t\$File\t"hl2mp\\openvibe_js_server.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_runtime.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_bindings.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_player.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\quickjs.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libregexp.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libunicode.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\cutils.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\dtoa.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libbf.c"\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 \ OpenVibe_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"++++if ! grep -q 'OpenVibe_OnClientModeInit' "$HL2MP_CLIENTMODE"; then++ sed -i '/#include "ienginevgui.h"/a void OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"++ sed -i '/BaseClass::Init();/a \ OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"++ echo "[openvibe-sdk] patched clientmode_hl2mpnormal.cpp OpenVibe menu hook"++fi++++perl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"++++if [[ -f "$GAMEINTERFACE" ]]; then++ if ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; then++ sed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"++ sed -i '/VPROF( "CServerGameDLL::GameFrame" );/a #ifdef HL2MP\n\tOpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"++ echo "[openvibe-sdk] patched gameinterface.cpp frame hook"++ fi++++ if ! grep -q 'OpenVibe_OnClientDisconnect' "$GAMEINTERFACE"; then++ perl -pi -e 's/void CServerGameClients::ClientDisconnect/#ifdef HL2MP\nvoid OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer );\n#endif\nvoid CServerGameClients::ClientDisconnect/' "$GAMEINTERFACE"++ perl -0777 -pi -e 's/(void CServerGameClients::ClientDisconnect\( edict_t \*pEdict \)\s*\{\s*extern bool\s+g_fGameOver;\s*CBasePlayer \*player = \( CBasePlayer \* \)CBaseEntity::Instance\( pEdict \);)/$1\n#ifdef HL2MP\n\tif ( player ) { OpenVibe_OnClientDisconnect( player ); }\n#endif/g' "$GAMEINTERFACE"++ echo "[openvibe-sdk] patched gameinterface.cpp disconnect hook"++ fi++fi++++if [[ -f "$HL2MP_PLAYER" ]]; then++ if ! grep -q 'OpenVibe_OnPlayerDeath' "$HL2MP_PLAYER"; then++ sed -i '/CHL2MP_Player::Event_Killed/i void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller );' "$HL2MP_PLAYER"++ sed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"++ echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"++ fi++++ if ! grep -q 'OpenVibeJS_Server_PlayerSpawn' "$HL2MP_PLAYER"; then++ sed -i '/CHL2MP_Player::Spawn/i void OpenVibeJS_Server_PlayerSpawn( CHL2MP_Player *pPlayer );' "$HL2MP_PLAYER"++ sed -i '0,/BaseClass::Spawn();/s//BaseClass::Spawn();\n\tOpenVibeJS_Server_PlayerSpawn( this );/' "$HL2MP_PLAYER"++ echo "[openvibe-sdk] patched hl2mp_player.cpp spawn hook"++ fi++fi++++if [[ -f "$HL2MP_CLIENT" ]] && ! grep -q 'OpenVibeJS_Server_PlayerSay' "$HL2MP_CLIENT"; then++ sed -i '/void Host_Say/a bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *pPlayer, const char *pszText );' "$HL2MP_CLIENT"++ python3 - "$HL2MP_CLIENT" <<'PY'++from pathlib import Path++import re, sys++++p = Path(sys.argv[1])++s = p.read_text()++++# Best-effort patch for common Source SDK 2013 HL2MP Host_Say shape:++# CHL2MP_Player *client = ...++# const char *p = args.ArgS();++if "OpenVibeJS_Server_PlayerSay( client," not in s:++ s2 = re.sub(++ r'(const char \*p\s*=\s*args\.ArgS\(\);\s*)',++ r'\1\n\tif ( client && OpenVibeJS_Server_PlayerSay( client, p ) )\n\t\treturn;\n',++ s,++ count=1,++ )++ if s2 != s:++ s = s2++ print("[openvibe-sdk] patched hl2mp_client.cpp PlayerSay hook")++ else:++ print("[openvibe-sdk] WARNING: could not auto-patch Host_Say PlayerSay body")++p.write_text(s)++PY++fi++++echo "[openvibe-sdk] Source SDK OpenVibe patch applied"nnew file mode 100755@@@ -0,0 +1,173 @@++#!/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_tree() {++ local src="$1"++ local dst="$2"++ mkdir -p "$dst"++ rsync -a --delete "$src/" "$dst/"++ echo "[openvibe-sdk] copied tree ${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"++++copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.h" \++ "$SDK/src/game/shared/openvibe/ov_js_runtime.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_runtime.cpp"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.h" \++ "$SDK/src/game/shared/openvibe/ov_js_bindings.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_bindings.cpp"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.h" \++ "$SDK/src/game/shared/openvibe/ov_js_player.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_player.cpp"++++copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.h" \++ "$SDK/src/game/server/hl2mp/openvibe_js_server.h"++copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp" \++ "$SDK/src/game/server/hl2mp/openvibe_js_server.cpp"++++copy_tree "$ROOT/sdk/openvibe/third_party/quickjs" \++ "$SDK/src/game/shared/openvibe/third_party/quickjs"++++QJS_C="$SDK/src/game/shared/openvibe/third_party/quickjs/quickjs.c"++if [[ -f "$QJS_C" ]] && ! grep -q 'OPENVIBE_QUICKJS_CONFIG' "$QJS_C"; then++ tmp="$(mktemp)"++ cat > "$tmp" <<'QCFG'++#ifndef OPENVIBE_QUICKJS_CONFIG++#define OPENVIBE_QUICKJS_CONFIG++#ifndef _GNU_SOURCE++#define _GNU_SOURCE++#endif++#ifndef CONFIG_VERSION++#define CONFIG_VERSION "openvibe"++#endif++#endif++++QCFG++ cat "$QJS_C" >> "$tmp"++ mv "$tmp" "$QJS_C"++fi++++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"++HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp"++GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"++HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.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/^.*hl2mp\\openvibe_js_server\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_bindings\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_player\.cpp.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\quickjs\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libregexp\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libunicode\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\cutils\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\dtoa\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libbf\.c.*\n//mg;++ s/(\$File\s+"hl2mp\\hl2mp_player\.h"\n)/$1\t\t\t\$File\t"hl2mp\\openvibe_server.cpp"\n\t\t\t\$File\t"hl2mp\\openvibe_js_server.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_runtime.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_bindings.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_player.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\quickjs.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libregexp.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libunicode.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\cutils.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\dtoa.c"\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 \ OpenVibe_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"++++if ! grep -q 'OpenVibe_OnClientModeInit' "$HL2MP_CLIENTMODE"; then++ sed -i '/#include "ienginevgui.h"/a void OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"++ sed -i '/BaseClass::Init();/a \ OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"++ echo "[openvibe-sdk] patched clientmode_hl2mpnormal.cpp OpenVibe menu hook"++fi++++perl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"++++if [[ -f "$GAMEINTERFACE" ]]; then++ if ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; then++ sed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"++ sed -i '/VPROF( "CServerGameDLL::GameFrame" );/a #ifdef HL2MP\n\tOpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"++ echo "[openvibe-sdk] patched gameinterface.cpp frame hook"++ fi++++ if ! grep -q 'OpenVibe_OnClientDisconnect' "$GAMEINTERFACE"; then++ perl -pi -e 's/void CServerGameClients::ClientDisconnect/#ifdef HL2MP\nvoid OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer );\n#endif\nvoid CServerGameClients::ClientDisconnect/' "$GAMEINTERFACE"++ perl -0777 -pi -e 's/(void CServerGameClients::ClientDisconnect\( edict_t \*pEdict \)\s*\{\s*extern bool\s+g_fGameOver;\s*CBasePlayer \*player = \( CBasePlayer \* \)CBaseEntity::Instance\( pEdict \);)/$1\n#ifdef HL2MP\n\tif ( player ) { OpenVibe_OnClientDisconnect( player ); }\n#endif/g' "$GAMEINTERFACE"++ echo "[openvibe-sdk] patched gameinterface.cpp disconnect hook"++ fi++fi++++if [[ -f "$HL2MP_PLAYER" ]]; then++ if ! grep -q 'OpenVibe_OnPlayerDeath' "$HL2MP_PLAYER"; then++ sed -i '/CHL2MP_Player::Event_Killed/i void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller );' "$HL2MP_PLAYER"++ sed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"++ echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"++ fi++++ if ! grep -q 'OpenVibeJS_Server_PlayerSpawn' "$HL2MP_PLAYER"; then++ sed -i '/CHL2MP_Player::Spawn/i void OpenVibeJS_Server_PlayerSpawn( CHL2MP_Player *pPlayer );' "$HL2MP_PLAYER"++ sed -i '0,/BaseClass::Spawn();/s//BaseClass::Spawn();\n\tOpenVibeJS_Server_PlayerSpawn( this );/' "$HL2MP_PLAYER"++ echo "[openvibe-sdk] patched hl2mp_player.cpp spawn hook"++ fi++fi++++if [[ -f "$HL2MP_CLIENT" ]] && ! grep -q 'OpenVibeJS_Server_PlayerSay' "$HL2MP_CLIENT"; then++ sed -i '/void Host_Say/a bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *pPlayer, const char *pszText );' "$HL2MP_CLIENT"++ python3 - "$HL2MP_CLIENT" <<'PY'++from pathlib import Path++import re, sys++++p = Path(sys.argv[1])++s = p.read_text()++++# Best-effort patch for common Source SDK 2013 HL2MP Host_Say shape:++# CHL2MP_Player *client = ...++# const char *p = args.ArgS();++if "OpenVibeJS_Server_PlayerSay( client," not in s:++ s2 = re.sub(++ r'(const char \*p\s*=\s*args\.ArgS\(\);\s*)',++ r'\1\n\tif ( client && OpenVibeJS_Server_PlayerSay( client, p ) )\n\t\treturn;\n',++ s,++ count=1,++ )++ if s2 != s:++ s = s2++ print("[openvibe-sdk] patched hl2mp_client.cpp PlayerSay hook")++ else:++ print("[openvibe-sdk] WARNING: could not auto-patch Host_Say PlayerSay body")++p.write_text(s)++PY++fi++++echo "[openvibe-sdk] Source SDK OpenVibe patch applied"nnew file mode 100755@@@ -0,0 +1,104 @@++#!/usr/bin/env bash++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++QJS="$ROOT/sdk/openvibe/third_party/quickjs"++++cd "$ROOT"++++echo "[openvibe] forcing clean QuickJS re-vendor"++rm -rf "$QJS"++tools/vendor-quickjs.sh++++echo "[openvibe] replacing smoke-quickjs.sh with robust version"++++cat > tools/smoke-quickjs.sh <<'SMOKE'++#!/usr/bin/env bash++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++QJS="$ROOT/sdk/openvibe/third_party/quickjs"++BUILD="$ROOT/.tmp/quickjs-smoke"++++rm -rf "$BUILD"++mkdir -p "$BUILD"++++cat > "$BUILD/smoke.c" <<'C'++#include <stdio.h>++#include "quickjs.h"++++int main(void) {++ JSRuntime *rt = JS_NewRuntime();++ JSContext *ctx = JS_NewContext(rt);++++ JSValue value = JS_Eval(ctx, "1 + 2 + 3", 9, "<smoke>", JS_EVAL_TYPE_GLOBAL);++ int32_t out = 0;++ JS_ToInt32(ctx, &out, value);++++ JS_FreeValue(ctx, value);++ JS_FreeContext(ctx);++ JS_FreeRuntime(rt);++++ printf("quickjs result=%d\n", out);++ return out == 6 ? 0 : 1;++}++C++++sources=(++ "$QJS/quickjs.c"++ "$QJS/libregexp.c"++ "$QJS/libunicode.c"++ "$QJS/cutils.c"++)++++[[ -f "$QJS/dtoa.c" ]] && sources+=("$QJS/dtoa.c")++[[ -f "$QJS/libbf.c" ]] && sources+=("$QJS/libbf.c")++++cc \++ -std=gnu11 \++ -D_GNU_SOURCE \++ -DCONFIG_VERSION=\"openvibe\" \++ -I"$QJS" \++ "$BUILD/smoke.c" \++ "${sources[@]}" \++ -lm \++ -ldl \++ -lpthread \++ -o "$BUILD/smoke-quickjs"++++"$BUILD/smoke-quickjs"++SMOKE++++chmod +x tools/smoke-quickjs.sh++++echo "[openvibe] patching apply-openvibe-sdk.sh to not require libbf.c blindly"++++python3 <<'PY'++from pathlib import Path++++p = Path("tools/apply-openvibe-sdk.sh")++s = p.read_text()++++# Remove hardcoded libbf VPC line insertion if present.++s = s.replace(++ '\\n\\t\\t\\t\\$File\\t"..\\\\shared\\\\openvibe\\\\third_party\\\\quickjs\\\\libbf.c"',++ ''++)++++# Also remove the cleanup regex for libbf so repeated runs don't matter.++s = s.replace(++ ' s/^.*openvibe\\\\\\\\third_party\\\\\\\\quickjs\\\\\\\\libbf\\\\.c.*\\n//mg;\\n',++ ''++)++++p.write_text(s)++PY++++echo "[openvibe] quickjs folder now contains:"++find "$QJS" -maxdepth 1 -type f -printf " - %f\n" | sort++++echo "[openvibe] running smoke test"++tools/smoke-quickjs.sh++++echo++echo "[openvibe] QuickJS vendor/smoke fixed."nnew file mode 100755@@@ -0,0 +1,1665 @@++#!/usr/bin/env bash++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++SDK="${OPENVIBE_SDK:-$ROOT/engine/source-sdk-2013}"++STAMP="$(date +%Y%m%d-%H%M%S)"++++cd "$ROOT"++++need_file() {++ [[ -f "$1" ]] || { echo "Missing required file: $1" >&2; exit 1; }++}++++backup_file() {++ local file="$1"++ [[ -f "$file" ]] && cp "$file" "$file.bak.$STAMP"++}++++mkdir -p \++ tools \++ sdk/openvibe/third_party/quickjs \++ sdk/openvibe/shared \++ sdk/openvibe/server/hl2mp \++ game/openvibe.games/js/core \++ game/openvibe.games/js/autorun/{server,client,shared} \++ game/openvibe.games/js/gamemodes/{base,hub,prophunt,deathrun,fortwars,traitortown} \++ game/openvibe.games/js/addons++++need_file "tools/apply-openvibe-sdk.sh"++need_file "sdk/openvibe/server/hl2mp/openvibe_server.cpp"++++echo "[openvibe] writing QuickJS vendor script"++++cat > tools/vendor-quickjs.sh <<'VENDOR_QJS'++#!/usr/bin/env bash++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++DEST="$ROOT/sdk/openvibe/third_party/quickjs"++TMP="${TMPDIR:-/tmp}/openvibe-quickjs-vendor"++++rm -rf "$TMP"++++git clone --depth 1 https://github.com/bellard/quickjs.git "$TMP" || \++git clone --depth 1 https://github.com/quickjs-ng/quickjs.git "$TMP"++++rm -rf "$DEST"++mkdir -p "$DEST"++++rsync -av \++ --include='quickjs.c' \++ --include='quickjs.h' \++ --include='quickjs-atom.h' \++ --include='quickjs-opcode.h' \++ --include='libregexp.c' \++ --include='libregexp.h' \++ --include='libregexp-opcode.h' \++ --include='libunicode.c' \++ --include='libunicode.h' \++ --include='libunicode-table.h' \++ --include='cutils.c' \++ --include='cutils.h' \++ --include='dtoa.c' \++ --include='dtoa.h' \++ --include='libbf.c' \++ --include='libbf.h' \++ --include='list.h' \++ --include='LICENSE' \++ --include='VERSION' \++ --exclude='*' \++ "$TMP/" \++ "$DEST/"++++git -C "$TMP" rev-parse HEAD > "$DEST/UPSTREAM_COMMIT"++++cat > "$DEST/README.openvibe.md" <<'DOC'++# QuickJS vendored for OpenVibe: Source++++Vendored QuickJS core files for OpenVibe's embedded JavaScript runtime.++++Excluded intentionally:++- qjs.c++- qjsc.c++- quickjs-libc.c++- quickjs-libc.h++++Reason:++OpenVibe embeds QuickJS as a sandboxed script VM. Community scripts should not++receive raw std/os/filesystem/process APIs.++DOC++++echo "[openvibe] vendored QuickJS into $DEST"++echo "[openvibe] upstream commit: $(cat "$DEST/UPSTREAM_COMMIT")"++VENDOR_QJS++++chmod +x tools/vendor-quickjs.sh++++echo "[openvibe] writing QuickJS smoke test"++++cat > tools/smoke-quickjs.sh <<'SMOKE_QJS'++#!/usr/bin/env bash++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++QJS="$ROOT/sdk/openvibe/third_party/quickjs"++BUILD="$ROOT/.tmp/quickjs-smoke"++++rm -rf "$BUILD"++mkdir -p "$BUILD"++++cat > "$BUILD/smoke.c" <<'C'++#include <stdio.h>++#include "quickjs.h"++++int main(void) {++ JSRuntime *rt = JS_NewRuntime();++ JSContext *ctx = JS_NewContext(rt);++++ JSValue value = JS_Eval(ctx, "1 + 2 + 3", 9, "<smoke>", JS_EVAL_TYPE_GLOBAL);++ int32_t out = 0;++ JS_ToInt32(ctx, &out, value);++++ JS_FreeValue(ctx, value);++ JS_FreeContext(ctx);++ JS_FreeRuntime(rt);++++ printf("quickjs result=%d\n", out);++ return out == 6 ? 0 : 1;++}++C++++cc \++ -std=c11 \++ -D_GNU_SOURCE \++ -DCONFIG_VERSION=\"openvibe\" \++ -I"$QJS" \++ "$BUILD/smoke.c" \++ "$QJS/quickjs.c" \++ "$QJS/libregexp.c" \++ "$QJS/libunicode.c" \++ "$QJS/cutils.c" \++ "$QJS/dtoa.c" \++ "$QJS/libbf.c" \++ -lm \++ -ldl \++ -lpthread \++ -o "$BUILD/smoke-quickjs"++++"$BUILD/smoke-quickjs"++SMOKE_QJS++++chmod +x tools/smoke-quickjs.sh++++if [[ ! -f sdk/openvibe/third_party/quickjs/quickjs.c ]]; then++ echo "[openvibe] vendoring QuickJS"++ tools/vendor-quickjs.sh++else++ echo "[openvibe] QuickJS already vendored; skipping vendor step"++fi++++echo "[openvibe] writing JS runtime files"++++cat > game/openvibe.games/js/core/hook.js <<'JS_HOOK'++(function () {++ const hooks = new Map();++++ function getEvent(name) {++ let map = hooks.get(name);++ if (!map) {++ map = new Map();++ hooks.set(name, map);++ }++ return map;++ }++++ globalThis.hook = {++ add(name, id, fn) {++ if (typeof name !== "string" || !name) throw new Error("hook name must be string");++ if (typeof id !== "string" || !id) throw new Error("hook id must be string");++ if (typeof fn !== "function") throw new Error("hook callback must be function");++ getEvent(name).set(id, fn);++ },++++ remove(name, id) {++ const map = hooks.get(name);++ return map ? map.delete(id) : false;++ },++++ run(name, ...args) {++ const map = hooks.get(name);++ if (!map) return undefined;++++ for (const [id, fn] of map.entries()) {++ try {++ const result = fn(...args);++ if (result !== undefined) return result;++ } catch (err) {++ OV.error(`[hook:${name}:${id}] ${err && err.stack ? err.stack : err}`);++ }++ }++++ return undefined;++ },++++ list() {++ const out = {};++ for (const [eventName, map] of hooks.entries()) out[eventName] = Array.from(map.keys());++ return out;++ },++++ clear() {++ hooks.clear();++ }++ };++})();++JS_HOOK++++cat > game/openvibe.games/js/core/gamemode.js <<'JS_GM'++(function () {++ let current = null;++++ globalThis.gamemode = {++ set(gm) {++ if (!gm || typeof gm !== "object") throw new Error("gamemode.set requires object");++ current = gm;++ },++++ get() {++ return current;++ },++++ call(name, ...args) {++ const hookResult = hook.run(name, ...args);++ if (hookResult !== undefined) return hookResult;++++ if (current && typeof current[name] === "function") {++ return current[name](...args);++ }++++ return undefined;++ }++ };++})();++JS_GM++++cat > game/openvibe.games/js/bridge.js <<'JS_BRIDGE'++OV.log("bridge.js loaded");++++globalThis.console = {++ log: (...args) => OV.log(args.map(String).join(" ")),++ warn: (...args) => OV.warn(args.map(String).join(" ")),++ error: (...args) => OV.error(args.map(String).join(" "))++};++++globalThis.game = {++ mode: () => OV.getMode(),++ map: () => OV.getMapName(),++ time: () => OV.time(),++ broadcast: (msg) => OV.broadcast(String(msg)),++ serverCommand: (cmd) => OV.serverCommand(String(cmd))++};++++globalThis.round = {++ start: () => OV.fireHook("RoundStart"),++ end: () => OV.fireHook("RoundEnd")++};++JS_BRIDGE++++cat > game/openvibe.games/js/gamemodes/base/server.js <<'JS_BASE'++const GM = {++ mode: "base",++ name: "OpenVibe Base",++++ Initialize() {++ OV.log("Base Initialize fired");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source.");++ },++++ PlayerSpawn(_ply) {},++++ PlayerDeath(_victim, _attacker) {},++++ PlayerDisconnected(_ply) {},++++ PlayerSay(_ply, _text) {++ return undefined;++ },++++ Think() {}++};++++gamemode.set(GM);++JS_BASE++++cat > game/openvibe.games/js/gamemodes/hub/server.js <<'JS_HUB'++const GM = {++ mode: "hub",++ name: "OpenVibe Hub",++++ Initialize() {++ OV.log("Hub Initialize fired");++ },++++ MapInitialize(mapName) {++ OV.log(`Map initialized: ${mapName}`);++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source JS runtime.");++ OV.broadcast(`${ply.name()} joined the hub.`);++ },++++ PlayerSpawn(ply) {++ ply.chat("PlayerSpawn hook fired.");++ },++++ PlayerSay(ply, text) {++ if (text === "!js") {++ ply.chat("JavaScript hooks are working.");++ return false;++ }++++ if (text === "!hp") {++ ply.chat(`Health: ${ply.health()}`);++ return false;++ }++++ if (text === "!players") {++ ply.chat(`Players online: ${OV.players().length}`);++ return false;++ }++++ return undefined;++ },++++ Think() {}++};++++gamemode.set(GM);++JS_HUB++++cat > game/openvibe.games/js/gamemodes/prophunt/server.js <<'JS_PH'++const props = ["crate", "barrel", "chair", "bucket"];++++function randomProp() {++ return props[Math.floor(Math.random() * props.length)];++}++++const GM = {++ mode: "prophunt",++ name: "OpenVibe Prop Hunt",++++ Initialize() {++ OV.log("Prop Hunt Initialize fired");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Prop Hunt: hide as props or hunt them down.");++ },++++ PlayerSay(ply, text) {++ if (text === "!prop") {++ ply.runCommand(`ov_prophunt_disguise ${randomProp()}`);++ return false;++ }++++ if (text.startsWith("!prop ")) {++ ply.runCommand(`ov_prophunt_disguise ${text.slice(6).trim()}`);++ return false;++ }++++ return undefined;++ },++++ Think() {}++};++++gamemode.set(GM);++JS_PH++++cat > game/openvibe.games/js/gamemodes/deathrun/server.js <<'JS_DR'++const GM = {++ mode: "deathrun",++ name: "OpenVibe Deathrun",++++ Initialize() {++ OV.log("Deathrun Initialize fired");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Deathrun: survive the traps and reach the finish.");++ },++++ PlayerSay(ply, text) {++ if (text === "!finish") {++ ply.chat("Deathrun finish test.");++ OV.reward(ply, 50, 100, "deathrun_finish");++ return false;++ }++++ return undefined;++ }++};++++gamemode.set(GM);++JS_DR++++cat > game/openvibe.games/js/gamemodes/fortwars/server.js <<'JS_FW'++const allowed = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);++++const GM = {++ mode: "fortwars",++ name: "OpenVibe Fort Wars",++++ Initialize() {++ OV.log("Fort Wars Initialize fired");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Fort Wars: build first, fight second.");++ },++++ PlayerSay(ply, text) {++ if (!text.startsWith("!build ")) return undefined;++++ const part = text.slice(7).trim();++ if (!allowed.has(part)) {++ ply.chat("Allowed: crate, barrel, pallet, fence, sheet");++ return false;++ }++++ ply.runCommand(`ov_fortwars_spawn ${part}`);++ return false;++ }++};++++gamemode.set(GM);++JS_FW++++cat > game/openvibe.games/js/gamemodes/traitortown/server.js <<'JS_TT'++const roles = new Map();++++const GM = {++ mode: "traitortown",++ name: "OpenVibe Traitor Town",++++ Initialize() {++ OV.log("Traitor Town Initialize fired");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Traitor Town: find the traitors before they find you.");++ roles.set(ply.userId(), "innocent");++ },++++ PlayerSay(ply, text) {++ if (text === "!role") {++ ply.chat(`Your role: ${roles.get(ply.userId()) || "none"}`);++ return false;++ }++++ return undefined;++ },++++ PlayerDeath(victim, attacker) {++ OV.broadcast(`${victim.name()} died.`);++ }++};++++gamemode.set(GM);++JS_TT++++echo "[openvibe] writing C++ embedded JS files"++++cat > sdk/openvibe/shared/ov_js_runtime.h <<'CPP_RT_H'++#pragma once++++#include "openvibe/third_party/quickjs/quickjs.h"++++class COpenVibeJSRuntime++{++public:++ COpenVibeJSRuntime();++ ~COpenVibeJSRuntime();++++ bool Init(bool bServerRealm, const char *pszMode);++ void Shutdown();++++ bool LoadFile(const char *pszPath);++ bool Eval(const char *pszCode, const char *pszFilename);++++ JSValue CallHookRaw(const char *pszHookName, int argc = 0, JSValueConst *argv = nullptr);++ void CallHookVoid(const char *pszHookName, int argc = 0, JSValueConst *argv = nullptr);++ bool CallHookBool(const char *pszHookName, bool *pOut, int argc = 0, JSValueConst *argv = nullptr);++++ JSContext *Context() { return m_pCtx; }++ const char *GetMode() const { return m_szMode; }++++private:++ void PrintException(const char *pszWhere);++ bool LoadCoreFiles();++ bool LoadGamemode();++++ JSRuntime *m_pRuntime = nullptr;++ JSContext *m_pCtx = nullptr;++ bool m_bServerRealm = false;++ char m_szMode[64]{};++};++CPP_RT_H++++cat > sdk/openvibe/shared/ov_js_runtime.cpp <<'CPP_RT_CPP'++#include "cbase.h"++#include "filesystem.h"++#include "ov_js_runtime.h"++#include "ov_js_bindings.h"++++#include "tier0/memdbgon.h"++++static int OV_JSInterruptHandler(JSRuntime *rt, void *opaque)++{++ return 0;++}++++COpenVibeJSRuntime::COpenVibeJSRuntime() {}++++COpenVibeJSRuntime::~COpenVibeJSRuntime()++{++ Shutdown();++}++++bool COpenVibeJSRuntime::Init(bool bServerRealm, const char *pszMode)++{++ Shutdown();++++ m_bServerRealm = bServerRealm;++ Q_strncpy(m_szMode, pszMode && pszMode[0] ? pszMode : "hub", sizeof(m_szMode));++++ m_pRuntime = JS_NewRuntime();++ if (!m_pRuntime)++ return false;++++ JS_SetMemoryLimit(m_pRuntime, 16 * 1024 * 1024);++ JS_SetMaxStackSize(m_pRuntime, 512 * 1024);++ JS_SetInterruptHandler(m_pRuntime, OV_JSInterruptHandler, this);++++ m_pCtx = JS_NewContext(m_pRuntime);++ if (!m_pCtx)++ {++ Shutdown();++ return false;++ }++++ OVJS_RegisterNativeBindings(m_pCtx, this);++++ if (!LoadCoreFiles())++ return false;++++ if (!LoadGamemode())++ return false;++++ return true;++}++++void COpenVibeJSRuntime::Shutdown()++{++ if (m_pCtx)++ {++ JS_FreeContext(m_pCtx);++ m_pCtx = nullptr;++ }++++ if (m_pRuntime)++ {++ JS_FreeRuntime(m_pRuntime);++ m_pRuntime = nullptr;++ }++}++++bool COpenVibeJSRuntime::Eval(const char *pszCode, const char *pszFilename)++{++ if (!m_pCtx || !pszCode)++ return false;++++ JSValue result = JS_Eval(++ m_pCtx,++ pszCode,++ Q_strlen(pszCode),++ pszFilename ? pszFilename : "<openvibe>",++ JS_EVAL_TYPE_GLOBAL++ );++++ if (JS_IsException(result))++ {++ PrintException(pszFilename);++ JS_FreeValue(m_pCtx, result);++ return false;++ }++++ JS_FreeValue(m_pCtx, result);++ return true;++}++++bool COpenVibeJSRuntime::LoadFile(const char *pszPath)++{++ FileHandle_t file = filesystem->Open(pszPath, "rb", "MOD");++ if (!file)++ {++ Warning("[OV JS] Could not open %s\n", pszPath);++ return false;++ }++++ int size = filesystem->Size(file);++ if (size <= 0 || size > 1024 * 1024)++ {++ filesystem->Close(file);++ Warning("[OV JS] Refusing script %s size=%d\n", pszPath, size);++ return false;++ }++++ char *buffer = new char[size + 1];++ filesystem->Read(buffer, size, file);++ filesystem->Close(file);++ buffer[size] = '\0';++++ bool ok = Eval(buffer, pszPath);++ delete[] buffer;++++ return ok;++}++++bool COpenVibeJSRuntime::LoadCoreFiles()++{++ if (!LoadFile("js/core/hook.js"))++ return false;++++ if (!LoadFile("js/core/gamemode.js"))++ return false;++++ if (!LoadFile("js/bridge.js"))++ return false;++++ return true;++}++++bool COpenVibeJSRuntime::LoadGamemode()++{++ LoadFile("js/gamemodes/base/server.js");++++ char path[256];++ Q_snprintf(path, sizeof(path), "js/gamemodes/%s/%s.js", m_szMode, m_bServerRealm ? "server" : "client");++++ return LoadFile(path);++}++++JSValue COpenVibeJSRuntime::CallHookRaw(const char *pszHookName, int argc, JSValueConst *argv)++{++ if (!m_pCtx)++ return JS_UNDEFINED;++++ JSValue global = JS_GetGlobalObject(m_pCtx);++ JSValue gamemode = JS_GetPropertyStr(m_pCtx, global, "gamemode");++ JSValue call = JS_GetPropertyStr(m_pCtx, gamemode, "call");++++ if (!JS_IsFunction(m_pCtx, call))++ {++ JS_FreeValue(m_pCtx, call);++ JS_FreeValue(m_pCtx, gamemode);++ JS_FreeValue(m_pCtx, global);++ return JS_UNDEFINED;++ }++++ JSValue *callArgs = new JSValue[argc + 1];++ callArgs[0] = JS_NewString(m_pCtx, pszHookName);++++ for (int i = 0; i < argc; ++i)++ callArgs[i + 1] = JS_DupValue(m_pCtx, argv[i]);++++ JSValue result = JS_Call(m_pCtx, call, gamemode, argc + 1, callArgs);++++ for (int i = 0; i < argc + 1; ++i)++ JS_FreeValue(m_pCtx, callArgs[i]);++++ delete[] callArgs;++++ JS_FreeValue(m_pCtx, call);++ JS_FreeValue(m_pCtx, gamemode);++ JS_FreeValue(m_pCtx, global);++++ if (JS_IsException(result))++ {++ PrintException(pszHookName);++ JS_FreeValue(m_pCtx, result);++ return JS_UNDEFINED;++ }++++ return result;++}++++void COpenVibeJSRuntime::CallHookVoid(const char *pszHookName, int argc, JSValueConst *argv)++{++ if (!m_pCtx)++ return;++++ JSValue result = CallHookRaw(pszHookName, argc, argv);++ JS_FreeValue(m_pCtx, result);++}++++bool COpenVibeJSRuntime::CallHookBool(const char *pszHookName, bool *pOut, int argc, JSValueConst *argv)++{++ if (!m_pCtx)++ return false;++++ JSValue result = CallHookRaw(pszHookName, argc, argv);++++ if (JS_IsUndefined(result))++ {++ JS_FreeValue(m_pCtx, result);++ return false;++ }++++ if (pOut)++ *pOut = JS_ToBool(m_pCtx, result) != 0;++++ JS_FreeValue(m_pCtx, result);++ return true;++}++++void COpenVibeJSRuntime::PrintException(const char *pszWhere)++{++ JSValue exception = JS_GetException(m_pCtx);++ const char *pszError = JS_ToCString(m_pCtx, exception);++++ Warning("[OV JS] Exception in %s: %s\n", pszWhere ? pszWhere : "<unknown>", pszError ? pszError : "<null>");++++ if (pszError)++ JS_FreeCString(m_pCtx, pszError);++++ JS_FreeValue(m_pCtx, exception);++}++CPP_RT_CPP++++cat > sdk/openvibe/shared/ov_js_bindings.h <<'CPP_BIND_H'++#pragma once++++#include "openvibe/third_party/quickjs/quickjs.h"++++class COpenVibeJSRuntime;++++void OVJS_RegisterNativeBindings(JSContext *ctx, COpenVibeJSRuntime *runtime);++CPP_BIND_H++++cat > sdk/openvibe/shared/ov_js_player.h <<'CPP_PLAYER_H'++#pragma once++++#include "openvibe/third_party/quickjs/quickjs.h"++++class CHL2MP_Player;++++void OVJS_RegisterPlayerClass(JSContext *ctx);++JSValue OVJS_NewPlayer(JSContext *ctx, CHL2MP_Player *player);++CHL2MP_Player *OVJS_ResolvePlayerByUserId(int userId);++CHL2MP_Player *OVJS_GetPlayerFromThis(JSContext *ctx, JSValueConst thisVal);++CPP_PLAYER_H++++cat > sdk/openvibe/shared/ov_js_player.cpp <<'CPP_PLAYER_CPP'++#include "cbase.h"++#include "hl2mp_player.h"++#include "player.h"++#include "util.h"++#include "ov_js_player.h"++++#include "tier0/memdbgon.h"++++struct OVJSPlayerHandle++{++ int userId;++ int entIndex;++};++++static JSClassID g_OVJSPlayerClassID;++++CHL2MP_Player *OVJS_ResolvePlayerByUserId(int userId)++{++ for (int i = 1; i <= gpGlobals->maxClients; ++i)++ {++ CBasePlayer *base = UTIL_PlayerByIndex(i);++ if (!base)++ continue;++++ CHL2MP_Player *player = ToHL2MPPlayer(base);++ if (!player)++ continue;++++ if (player->GetUserID() == userId)++ return player;++ }++++ return nullptr;++}++++static void OVJS_PlayerFinalizer(JSRuntime *rt, JSValue val)++{++ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque(val, g_OVJSPlayerClassID));++ delete handle;++}++++CHL2MP_Player *OVJS_GetPlayerFromThis(JSContext *ctx, JSValueConst thisVal)++{++ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));++ if (!handle)++ return nullptr;++++ CHL2MP_Player *player = OVJS_ResolvePlayerByUserId(handle->userId);++ if (!player)++ return nullptr;++++ if (player->entindex() != handle->entIndex)++ return nullptr;++++ return player;++}++++static JSValue OVJS_Player_userId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));++ return handle ? JS_NewInt32(ctx, handle->userId) : JS_UNDEFINED;++}++++static JSValue OVJS_Player_entIndex(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));++ return handle ? JS_NewInt32(ctx, handle->entIndex) : JS_UNDEFINED;++}++++static JSValue OVJS_Player_name(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ return JS_NewString(ctx, player ? player->GetPlayerName() : "");++}++++static JSValue OVJS_Player_steamId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ if (!player)++ return JS_NewString(ctx, "");++++ const char *netid = engine->GetPlayerNetworkIDString(player->edict());++ return JS_NewString(ctx, netid ? netid : "");++}++++static JSValue OVJS_Player_health(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ return JS_NewInt32(ctx, player ? player->GetHealth() : 0);++}++++static JSValue OVJS_Player_setHealth(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ if (!player || argc < 1)++ return JS_UNDEFINED;++++ int32 value = 0;++ JS_ToInt32(ctx, &value, argv[0]);++++ if (value < 0) value = 0;++ if (value > 1000) value = 1000;++++ player->SetHealth(value);++ return JS_UNDEFINED;++}++++static JSValue OVJS_Player_team(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ return JS_NewInt32(ctx, player ? player->GetTeamNumber() : 0);++}++++static JSValue OVJS_Player_setTeam(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ if (!player || argc < 1)++ return JS_UNDEFINED;++++ int32 team = 0;++ JS_ToInt32(ctx, &team, argv[0]);++++ if (team >= 0 && team <= 4)++ player->ChangeTeam(team);++++ return JS_UNDEFINED;++}++++static JSValue OVJS_Player_chat(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ if (!player || argc < 1)++ return JS_UNDEFINED;++++ const char *msg = JS_ToCString(ctx, argv[0]);++ if (!msg)++ return JS_UNDEFINED;++++ ClientPrint(player, HUD_PRINTTALK, msg);++++ JS_FreeCString(ctx, msg);++ return JS_UNDEFINED;++}++++static JSValue OVJS_Player_runCommand(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);++ if (!player || argc < 1)++ return JS_UNDEFINED;++++ const char *cmd = JS_ToCString(ctx, argv[0]);++ if (!cmd)++ return JS_UNDEFINED;++++ if (!Q_strnicmp(cmd, "ov_", 3))++ engine->ClientCommand(player->edict(), "%s\n", cmd);++ else++ Warning("[OV JS] blocked player command: %s\n", cmd);++++ JS_FreeCString(ctx, cmd);++ return JS_UNDEFINED;++}++++static const JSCFunctionListEntry OVJSPlayerProtoFuncs[] =++{++ JS_CFUNC_DEF("userId", 0, OVJS_Player_userId),++ JS_CFUNC_DEF("entIndex", 0, OVJS_Player_entIndex),++ JS_CFUNC_DEF("steamId", 0, OVJS_Player_steamId),++ JS_CFUNC_DEF("name", 0, OVJS_Player_name),++ JS_CFUNC_DEF("health", 0, OVJS_Player_health),++ JS_CFUNC_DEF("setHealth", 1, OVJS_Player_setHealth),++ JS_CFUNC_DEF("team", 0, OVJS_Player_team),++ JS_CFUNC_DEF("setTeam", 1, OVJS_Player_setTeam),++ JS_CFUNC_DEF("chat", 1, OVJS_Player_chat),++ JS_CFUNC_DEF("runCommand", 1, OVJS_Player_runCommand),++};++++void OVJS_RegisterPlayerClass(JSContext *ctx)++{++ JSRuntime *rt = JS_GetRuntime(ctx);++++ JS_NewClassID(&g_OVJSPlayerClassID);++++ JSClassDef cls = {};++ cls.class_name = "OpenVibePlayer";++ cls.finalizer = OVJS_PlayerFinalizer;++++ JS_NewClass(rt, g_OVJSPlayerClassID, &cls);++++ JSValue proto = JS_NewObject(ctx);++ JS_SetPropertyFunctionList(ctx, proto, OVJSPlayerProtoFuncs, ARRAYSIZE(OVJSPlayerProtoFuncs));++ JS_SetClassProto(ctx, g_OVJSPlayerClassID, proto);++}++++JSValue OVJS_NewPlayer(JSContext *ctx, CHL2MP_Player *player)++{++ if (!player)++ return JS_NULL;++++ JSValue obj = JS_NewObjectClass(ctx, g_OVJSPlayerClassID);++ if (JS_IsException(obj))++ return obj;++++ OVJSPlayerHandle *handle = new OVJSPlayerHandle();++ handle->userId = player->GetUserID();++ handle->entIndex = player->entindex();++++ JS_SetOpaque(obj, handle);++ return obj;++}++CPP_PLAYER_CPP++++cat > sdk/openvibe/shared/ov_js_bindings.cpp <<'CPP_BIND_CPP'++#include "cbase.h"++#include "ov_js_bindings.h"++#include "ov_js_runtime.h"++#include "ov_js_player.h"++#include "hl2mp_player.h"++#include "util.h"++++#include "tier0/memdbgon.h"++++static COpenVibeJSRuntime *g_OVRuntime = nullptr;++++static JSValue OVJS_log(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_UNDEFINED;++ const char *msg = JS_ToCString(ctx, argv[0]);++ if (msg) { Msg("[OV JS] %s\n", msg); JS_FreeCString(ctx, msg); }++ return JS_UNDEFINED;++}++++static JSValue OVJS_warn(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_UNDEFINED;++ const char *msg = JS_ToCString(ctx, argv[0]);++ if (msg) { Warning("[OV JS] %s\n", msg); JS_FreeCString(ctx, msg); }++ return JS_UNDEFINED;++}++++static JSValue OVJS_error(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_UNDEFINED;++ const char *msg = JS_ToCString(ctx, argv[0]);++ if (msg) { Warning("[OV JS ERROR] %s\n", msg); JS_FreeCString(ctx, msg); }++ return JS_UNDEFINED;++}++++static JSValue OVJS_getMode(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ return JS_NewString(ctx, g_OVRuntime ? g_OVRuntime->GetMode() : "unknown");++}++++static JSValue OVJS_getMapName(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ return JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");++}++++static JSValue OVJS_time(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ return JS_NewFloat64(ctx, gpGlobals ? gpGlobals->curtime : 0.0);++}++++static JSValue OVJS_players(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ JSValue arr = JS_NewArray(ctx);++ uint32 index = 0;++++ for (int i = 1; i <= gpGlobals->maxClients; ++i)++ {++ CBasePlayer *base = UTIL_PlayerByIndex(i);++ if (!base) continue;++++ CHL2MP_Player *player = ToHL2MPPlayer(base);++ if (!player) continue;++++ JS_SetPropertyUint32(ctx, arr, index++, OVJS_NewPlayer(ctx, player));++ }++++ return arr;++}++++static JSValue OVJS_playerByUserId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_NULL;++++ int32 userId = 0;++ JS_ToInt32(ctx, &userId, argv[0]);++++ CHL2MP_Player *player = OVJS_ResolvePlayerByUserId(userId);++ return player ? OVJS_NewPlayer(ctx, player) : JS_NULL;++}++++static JSValue OVJS_broadcast(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_UNDEFINED;++++ const char *msg = JS_ToCString(ctx, argv[0]);++ if (!msg) return JS_UNDEFINED;++++ UTIL_ClientPrintAll(HUD_PRINTTALK, msg);++++ JS_FreeCString(ctx, msg);++ return JS_UNDEFINED;++}++++static bool OVJS_IsAllowedServerCommand(const char *cmd)++{++ return cmd && (!Q_strnicmp(cmd, "ov_", 3) || !Q_strnicmp(cmd, "say ", 4));++}++++static JSValue OVJS_serverCommand(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (argc < 1) return JS_UNDEFINED;++++ const char *cmd = JS_ToCString(ctx, argv[0]);++ if (!cmd) return JS_UNDEFINED;++++ if (OVJS_IsAllowedServerCommand(cmd))++ engine->ServerCommand(UTIL_VarArgs("%s\n", cmd));++ else++ Warning("[OV JS] blocked serverCommand: %s\n", cmd);++++ JS_FreeCString(ctx, cmd);++ return JS_UNDEFINED;++}++++static JSValue OVJS_fireHook(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ if (!g_OVRuntime || argc < 1) return JS_UNDEFINED;++++ const char *hookName = JS_ToCString(ctx, argv[0]);++ if (!hookName) return JS_UNDEFINED;++++ JSValue result = g_OVRuntime->CallHookRaw(hookName, argc - 1, argv + 1);++++ JS_FreeCString(ctx, hookName);++ return result;++}++++static JSValue OVJS_reward(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ Warning("[OV JS] OV.reward stubbed; backend reward wiring comes next.\n");++ return JS_UNDEFINED;++}++++static JSValue OVJS_endMatch(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ Warning("[OV JS] OV.endMatch stubbed; backend match wiring comes next.\n");++ return JS_UNDEFINED;++}++++static const JSCFunctionListEntry OVFuncs[] =++{++ JS_CFUNC_DEF("log", 1, OVJS_log),++ JS_CFUNC_DEF("warn", 1, OVJS_warn),++ JS_CFUNC_DEF("error", 1, OVJS_error),++ JS_CFUNC_DEF("getMode", 0, OVJS_getMode),++ JS_CFUNC_DEF("getMapName", 0, OVJS_getMapName),++ JS_CFUNC_DEF("time", 0, OVJS_time),++ JS_CFUNC_DEF("players", 0, OVJS_players),++ JS_CFUNC_DEF("playerByUserId", 1, OVJS_playerByUserId),++ JS_CFUNC_DEF("broadcast", 1, OVJS_broadcast),++ JS_CFUNC_DEF("serverCommand", 1, OVJS_serverCommand),++ JS_CFUNC_DEF("fireHook", 1, OVJS_fireHook),++ JS_CFUNC_DEF("reward", 4, OVJS_reward),++ JS_CFUNC_DEF("endMatch", 1, OVJS_endMatch),++};++++void OVJS_RegisterNativeBindings(JSContext *ctx, COpenVibeJSRuntime *runtime)++{++ g_OVRuntime = runtime;++++ OVJS_RegisterPlayerClass(ctx);++++ JSValue global = JS_GetGlobalObject(ctx);++ JSValue ov = JS_NewObject(ctx);++++ JS_SetPropertyFunctionList(ctx, ov, OVFuncs, ARRAYSIZE(OVFuncs));++ JS_SetPropertyStr(ctx, global, "OV", ov);++++ JS_FreeValue(ctx, global);++}++CPP_BIND_CPP++++cat > sdk/openvibe/server/hl2mp/openvibe_js_server.h <<'CPP_JS_SERVER_H'++#pragma once++++class CHL2MP_Player;++class CBasePlayer;++class CBaseEntity;++++void OpenVibeJS_ServerInit();++void OpenVibeJS_ServerShutdown();++void OpenVibeJS_ServerThink();++++void OpenVibeJS_Server_PlayerInitialSpawn(CHL2MP_Player *player);++void OpenVibeJS_Server_PlayerSpawn(CHL2MP_Player *player);++void OpenVibeJS_Server_PlayerDeath(CHL2MP_Player *victim, CBaseEntity *attacker, CBaseEntity *inflictor);++void OpenVibeJS_Server_PlayerDisconnected(CHL2MP_Player *player);++bool OpenVibeJS_Server_PlayerSay(CHL2MP_Player *player, const char *text);++CPP_JS_SERVER_H++++cat > sdk/openvibe/server/hl2mp/openvibe_js_server.cpp <<'CPP_JS_SERVER_CPP'++#include "cbase.h"++#include "hl2mp_player.h"++#include "openvibe_js_server.h"++#include "openvibe/ov_js_runtime.h"++#include "openvibe/ov_js_player.h"++++#include "tier0/memdbgon.h"++++static ConVar ov_mode(++ "ov_mode",++ "hub",++ FCVAR_GAMEDLL,++ "OpenVibe mode: hub, prophunt, deathrun, fortwars, traitortown." );++++static ConVar ov_js_enabled(++ "ov_js_enabled",++ "1",++ FCVAR_GAMEDLL,++ "Enable OpenVibe JavaScript runtime." );++++static COpenVibeJSRuntime g_OVServerJS;++static bool g_OVServerJSStarted = false;++++static bool OpenVibeJS_IsRunning()++{++ return ov_js_enabled.GetBool() && g_OVServerJS.Context() != nullptr;++}++++static void OpenVibeJS_EnsureStarted()++{++ if (g_OVServerJSStarted)++ return;++++ g_OVServerJSStarted = true;++++ if (!ov_js_enabled.GetBool())++ {++ Msg("[OV JS] disabled by ov_js_enabled=0\n");++ return;++ }++++ if (g_OVServerJS.Init(true, ov_mode.GetString()))++ {++ Msg("[OV JS] server runtime initialized for mode '%s'\n", ov_mode.GetString());++ g_OVServerJS.CallHookVoid("Initialize");++++ JSContext *ctx = g_OVServerJS.Context();++ JSValue mapName = JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");++ JSValueConst argv[] = { mapName };++ g_OVServerJS.CallHookVoid("MapInitialize", 1, argv);++ JS_FreeValue(ctx, mapName);++ }++ else++ {++ Warning("[OV JS] server runtime failed to initialize\n");++ }++}++++void OpenVibeJS_ServerInit()++{++ OpenVibeJS_EnsureStarted();++}++++void OpenVibeJS_ServerShutdown()++{++ if (OpenVibeJS_IsRunning())++ g_OVServerJS.CallHookVoid("Shutdown");++++ g_OVServerJS.Shutdown();++ g_OVServerJSStarted = false;++}++++void OpenVibeJS_ServerThink()++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning())++ return;++++ g_OVServerJS.CallHookVoid("Think");++}++++void OpenVibeJS_Server_PlayerInitialSpawn(CHL2MP_Player *player)++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning() || !player)++ return;++++ JSContext *ctx = g_OVServerJS.Context();++ JSValue ply = OVJS_NewPlayer(ctx, player);++++ JSValueConst argv[] = { ply };++ g_OVServerJS.CallHookVoid("PlayerInitialSpawn", 1, argv);++++ JS_FreeValue(ctx, ply);++}++++void OpenVibeJS_Server_PlayerSpawn(CHL2MP_Player *player)++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning() || !player)++ return;++++ JSContext *ctx = g_OVServerJS.Context();++ JSValue ply = OVJS_NewPlayer(ctx, player);++++ JSValueConst argv[] = { ply };++ g_OVServerJS.CallHookVoid("PlayerSpawn", 1, argv);++++ JS_FreeValue(ctx, ply);++}++++void OpenVibeJS_Server_PlayerDeath(CHL2MP_Player *victim, CBaseEntity *attacker, CBaseEntity *inflictor)++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning() || !victim)++ return;++++ JSContext *ctx = g_OVServerJS.Context();++++ JSValue jsVictim = OVJS_NewPlayer(ctx, victim);++ JSValue jsAttacker = JS_NULL;++++ if (attacker && attacker->IsPlayer())++ jsAttacker = OVJS_NewPlayer(ctx, ToHL2MPPlayer(static_cast<CBasePlayer *>(attacker)));++++ JSValueConst argv[] = { jsVictim, jsAttacker };++ g_OVServerJS.CallHookVoid("PlayerDeath", 2, argv);++++ JS_FreeValue(ctx, jsVictim);++ JS_FreeValue(ctx, jsAttacker);++}++++void OpenVibeJS_Server_PlayerDisconnected(CHL2MP_Player *player)++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning() || !player)++ return;++++ JSContext *ctx = g_OVServerJS.Context();++ JSValue ply = OVJS_NewPlayer(ctx, player);++++ JSValueConst argv[] = { ply };++ g_OVServerJS.CallHookVoid("PlayerDisconnected", 1, argv);++++ JS_FreeValue(ctx, ply);++}++++bool OpenVibeJS_Server_PlayerSay(CHL2MP_Player *player, const char *text)++{++ OpenVibeJS_EnsureStarted();++++ if (!OpenVibeJS_IsRunning() || !player || !text)++ return false;++++ JSContext *ctx = g_OVServerJS.Context();++++ JSValue ply = OVJS_NewPlayer(ctx, player);++ JSValue msg = JS_NewString(ctx, text);++++ JSValueConst argv[] = { ply, msg };++++ bool value = false;++ bool returned = g_OVServerJS.CallHookBool("PlayerSay", &value, 2, argv);++++ JS_FreeValue(ctx, ply);++ JS_FreeValue(ctx, msg);++++ return returned && value == false;++}++++static void OV_JSReload_f()++{++ OpenVibeJS_ServerShutdown();++ OpenVibeJS_ServerInit();++ Msg("[OV JS] reloaded\n");++}++++static ConCommand ov_js_reload(++ "ov_js_reload",++ OV_JSReload_f,++ "Reload OpenVibe JavaScript runtime.",++ FCVAR_GAMEDLL++);++CPP_JS_SERVER_CPP++++echo "[openvibe] replacing apply-openvibe-sdk.sh"++++backup_file tools/apply-openvibe-sdk.sh++++cat > tools/apply-openvibe-sdk.sh <<'APPLY'++#!/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_tree() {++ local src="$1"++ local dst="$2"++ mkdir -p "$dst"++ rsync -a --delete "$src/" "$dst/"++ echo "[openvibe-sdk] copied tree ${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"++++copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.h" \++ "$SDK/src/game/shared/openvibe/ov_js_runtime.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_runtime.cpp"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.h" \++ "$SDK/src/game/shared/openvibe/ov_js_bindings.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_bindings.cpp"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.h" \++ "$SDK/src/game/shared/openvibe/ov_js_player.h"++copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.cpp" \++ "$SDK/src/game/shared/openvibe/ov_js_player.cpp"++++copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.h" \++ "$SDK/src/game/server/hl2mp/openvibe_js_server.h"++copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp" \++ "$SDK/src/game/server/hl2mp/openvibe_js_server.cpp"++++copy_tree "$ROOT/sdk/openvibe/third_party/quickjs" \++ "$SDK/src/game/shared/openvibe/third_party/quickjs"++++QJS_C="$SDK/src/game/shared/openvibe/third_party/quickjs/quickjs.c"++if [[ -f "$QJS_C" ]] && ! grep -q 'OPENVIBE_QUICKJS_CONFIG' "$QJS_C"; then++ tmp="$(mktemp)"++ cat > "$tmp" <<'QCFG'++#ifndef OPENVIBE_QUICKJS_CONFIG++#define OPENVIBE_QUICKJS_CONFIG++#ifndef _GNU_SOURCE++#define _GNU_SOURCE++#endif++#ifndef CONFIG_VERSION++#define CONFIG_VERSION "openvibe"++#endif++#endif++++QCFG++ cat "$QJS_C" >> "$tmp"++ mv "$tmp" "$QJS_C"++fi++++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"++HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp"++GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"++HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.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/^.*hl2mp\\openvibe_js_server\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_bindings\.cpp.*\n//mg;++ s/^.*openvibe\\ov_js_player\.cpp.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\quickjs\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libregexp\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libunicode\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\cutils\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\dtoa\.c.*\n//mg;++ s/^.*openvibe\\third_party\\quickjs\\libbf\.c.*\n//mg;++ s/(\$File\s+"hl2mp\\hl2mp_player\.h"\n)/$1\t\t\t\$File\t"hl2mp\\openvibe_server.cpp"\n\t\t\t\$File\t"hl2mp\\openvibe_js_server.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_runtime.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_bindings.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_player.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\quickjs.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libregexp.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libunicode.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\cutils.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\dtoa.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libbf.c"\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 \ OpenVibe_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"++++if ! grep -q 'OpenVibe_OnClientModeInit' "$HL2MP_CLIENTMODE"; then++ sed -i '/#include "ienginevgui.h"/a void OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"++ sed -i '/BaseClass::Init();/a \ OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"++ echo "[openvibe-sdk] patched clientmode_hl2mpnormal.cpp OpenVibe menu hook"++fi++++perl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"++++if [[ -f "$GAMEINTERFACE" ]]; then++ if ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; then++ sed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"++ sed -i '/VPROF( "CServerGameDLL::GameFrame" );/a #ifdef HL2MP\n\tOpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"++ echo "[openvibe-sdk] patched gameinterface.cpp frame hook"++ fi++++ if ! grep -q 'OpenVibe_OnClientDisconnect' "$GAMEINTERFACE"; then++ perl -pi -e 's/void CServerGameClients::ClientDisconnect/#ifdef HL2MP\nvoid OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer );\n#endif\nvoid CServerGameClients::ClientDisconnect/' "$GAMEINTERFACE"++ perl -0777 -pi -e 's/(void CServerGameClients::ClientDisconnect\( edict_t \*pEdict \)\s*\{\s*extern bool\s+g_fGameOver;\s*CBasePlayer \*player = \( CBasePlayer \* \)CBaseEntity::Instance\( pEdict \);)/$1\n#ifdef HL2MP\n\tif ( player ) { OpenVibe_OnClientDisconnect( player ); }\n#endif/g' "$GAMEINTERFACE"++ echo "[openvibe-sdk] patched gameinterface.cpp disconnect hook"++ fi++fi++++if [[ -f "$HL2MP_PLAYER" ]]; then++ if ! grep -q 'OpenVibe_OnPlayerDeath' "$HL2MP_PLAYER"; then++ sed -i '/CHL2MP_Player::Event_Killed/i void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller );' "$HL2MP_PLAYER"++ sed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"++ echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"++ fi++++ if ! grep -q 'OpenVibeJS_Server_PlayerSpawn' "$HL2MP_PLAYER"; then++ sed -i '/CHL2MP_Player::Spawn/i void OpenVibeJS_Server_PlayerSpawn( CHL2MP_Player *pPlayer );' "$HL2MP_PLAYER"++ sed -i '0,/BaseClass::Spawn();/s//BaseClass::Spawn();\n\tOpenVibeJS_Server_PlayerSpawn( this );/' "$HL2MP_PLAYER"++ echo "[openvibe-sdk] patched hl2mp_player.cpp spawn hook"++ fi++fi++++if [[ -f "$HL2MP_CLIENT" ]] && ! grep -q 'OpenVibeJS_Server_PlayerSay' "$HL2MP_CLIENT"; then++ sed -i '/void Host_Say/a bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *pPlayer, const char *pszText );' "$HL2MP_CLIENT"++ python3 - "$HL2MP_CLIENT" <<'PY'++from pathlib import Path++import re, sys++++p = Path(sys.argv[1])++s = p.read_text()++++# Best-effort patch for common Source SDK 2013 HL2MP Host_Say shape:++# CHL2MP_Player *client = ...++# const char *p = args.ArgS();++if "OpenVibeJS_Server_PlayerSay( client," not in s:++ s2 = re.sub(++ r'(const char \*p\s*=\s*args\.ArgS\(\);\s*)',++ r'\1\n\tif ( client && OpenVibeJS_Server_PlayerSay( client, p ) )\n\t\treturn;\n',++ s,++ count=1,++ )++ if s2 != s:++ s = s2++ print("[openvibe-sdk] patched hl2mp_client.cpp PlayerSay hook")++ else:++ print("[openvibe-sdk] WARNING: could not auto-patch Host_Say PlayerSay body")++p.write_text(s)++PY++fi++++echo "[openvibe-sdk] Source SDK OpenVibe patch applied"++APPLY++++chmod +x tools/apply-openvibe-sdk.sh++++echo "[openvibe] patching sdk/openvibe/server/hl2mp/openvibe_server.cpp"++++backup_file sdk/openvibe/server/hl2mp/openvibe_server.cpp++++python3 <<'PY'++from pathlib import Path++import re++++path = Path("sdk/openvibe/server/hl2mp/openvibe_server.cpp")++s = path.read_text()++++if '#include "openvibe_js_server.h"' not in s:++ # Put it after hl2mp_player include if possible, else after first include.++ s = re.sub(++ r'(#include\s+"hl2mp_player\.h"\s*\n)',++ r'\1#include "openvibe_js_server.h"\n',++ s,++ count=1,++ )++ if '#include "openvibe_js_server.h"' not in s:++ s = re.sub(r'(#include\s+".*?"\s*\n)', r'\1#include "openvibe_js_server.h"\n', s, count=1)++++def replace_function_body(src, signature_regex, new_body):++ m = re.search(signature_regex, src)++ if not m:++ print(f"[openvibe] WARNING: missing function matching {signature_regex}")++ return src++++ brace = src.find("{", m.end())++ if brace < 0:++ print(f"[openvibe] WARNING: missing opening brace for {signature_regex}")++ return src++++ depth = 0++ end = None++ for i in range(brace, len(src)):++ if src[i] == "{":++ depth += 1++ elif src[i] == "}":++ depth -= 1++ if depth == 0:++ end = i++ break++++ if end is None:++ print(f"[openvibe] WARNING: missing closing brace for {signature_regex}")++ return src++++ return src[:brace + 1] + "\n" + new_body.rstrip() + "\n" + src[end:]++++s = replace_function_body(++ s,++ r'void\s+OpenVibe_OnFrame\s*\(\s*\)',++ """++ OpenVibeJS_ServerThink();++"""++)++++s = replace_function_body(++ s,++ r'void\s+OpenVibe_OnClientDisconnect\s*\(\s*CBasePlayer\s*\*\s*pPlayer\s*\)',++ """++ CHL2MP_Player *pHL2MP = ToHL2MPPlayer( pPlayer );++ if ( pHL2MP )++ OpenVibeJS_Server_PlayerDisconnected( pHL2MP );++"""++)++++s = replace_function_body(++ s,++ r'void\s+OpenVibe_OnPlayerDeath\s*\(\s*CHL2MP_Player\s*\*\s*pPlayer\s*,\s*CBaseEntity\s*\*\s*pKiller\s*\)',++ """++ OpenVibeJS_Server_PlayerDeath( pPlayer, pKiller, NULL );++"""++)++++if "OpenVibeJS_Server_PlayerInitialSpawn( pPlayer );" not in s:++ m = re.search(r'void\s+OpenVibe_OnClientActive\s*\(\s*CHL2MP_Player\s*\*\s*pPlayer\s*\)', s)++ if m:++ brace = s.find("{", m.end())++ depth = 0++ end = None++ for i in range(brace, len(s)):++ if s[i] == "{":++ depth += 1++ elif s[i] == "}":++ depth -= 1++ if depth == 0:++ end = i++ break++ if end:++ s = s[:end] + "\n OpenVibeJS_Server_PlayerInitialSpawn( pPlayer );\n" + s[end:]++ print("[openvibe] patched OpenVibe_OnClientActive -> PlayerInitialSpawn")++ else:++ print("[openvibe] WARNING: could not patch OpenVibe_OnClientActive body")++ else:++ print("[openvibe] WARNING: OpenVibe_OnClientActive not found")++++path.write_text(s)++PY++++echo "[openvibe] running QuickJS smoke test"++++tools/smoke-quickjs.sh++++echo++echo "[openvibe] embedded JS bootstrap files written."++echo++echo "Next:"++echo " tools/apply-openvibe-sdk.sh"++echo " tools/build-sdk-linux.sh"++echo " tools/setup-openvibe-bin.sh"++echo " OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh"++echo++echo "Then in server console:"++echo " ov_js_reload"++echo++echo "Then in game:"++echo " connect 127.0.0.1:27015"++echo " say !js"++echo " say !hp"nnew file mode 100755@@@ -0,0 +1,54 @@++#!/usr/bin/env bash++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++QJS="$ROOT/sdk/openvibe/third_party/quickjs"++BUILD="$ROOT/.tmp/quickjs-smoke"++++rm -rf "$BUILD"++mkdir -p "$BUILD"++++cat > "$BUILD/smoke.c" <<'C'++#include <stdio.h>++#include "quickjs.h"++++int main(void) {++ JSRuntime *rt = JS_NewRuntime();++ JSContext *ctx = JS_NewContext(rt);++++ JSValue value = JS_Eval(ctx, "1 + 2 + 3", 9, "<smoke>", JS_EVAL_TYPE_GLOBAL);++ int32_t out = 0;++ JS_ToInt32(ctx, &out, value);++++ JS_FreeValue(ctx, value);++ JS_FreeContext(ctx);++ JS_FreeRuntime(rt);++++ printf("quickjs result=%d\n", out);++ return out == 6 ? 0 : 1;++}++C++++sources=(++ "$QJS/quickjs.c"++ "$QJS/libregexp.c"++ "$QJS/libunicode.c"++ "$QJS/cutils.c"++)++++[[ -f "$QJS/dtoa.c" ]] && sources+=("$QJS/dtoa.c")++[[ -f "$QJS/libbf.c" ]] && sources+=("$QJS/libbf.c")++++cc \++ -std=gnu11 \++ -D_GNU_SOURCE \++ -DCONFIG_VERSION=\"openvibe\" \++ -I"$QJS" \++ "$BUILD/smoke.c" \++ "${sources[@]}" \++ -lm \++ -ldl \++ -lpthread \++ -o "$BUILD/smoke-quickjs"++++"$BUILD/smoke-quickjs"nnew file mode 100755@@@ -0,0 +1,59 @@++#!/usr/bin/env bash++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++DEST="$ROOT/sdk/openvibe/third_party/quickjs"++TMP="${TMPDIR:-/tmp}/openvibe-quickjs-vendor"++++rm -rf "$TMP"++++git clone --depth 1 https://github.com/bellard/quickjs.git "$TMP" || \++git clone --depth 1 https://github.com/quickjs-ng/quickjs.git "$TMP"++++rm -rf "$DEST"++mkdir -p "$DEST"++++rsync -av \++ --include='quickjs.c' \++ --include='quickjs.h' \++ --include='quickjs-atom.h' \++ --include='quickjs-opcode.h' \++ --include='libregexp.c' \++ --include='libregexp.h' \++ --include='libregexp-opcode.h' \++ --include='libunicode.c' \++ --include='libunicode.h' \++ --include='libunicode-table.h' \++ --include='cutils.c' \++ --include='cutils.h' \++ --include='dtoa.c' \++ --include='dtoa.h' \++ --include='libbf.c' \++ --include='libbf.h' \++ --include='list.h' \++ --include='LICENSE' \++ --include='VERSION' \++ --exclude='*' \++ "$TMP/" \++ "$DEST/"++++git -C "$TMP" rev-parse HEAD > "$DEST/UPSTREAM_COMMIT"++++cat > "$DEST/README.openvibe.md" <<'DOC'++# QuickJS vendored for OpenVibe: Source++++Vendored QuickJS core files for OpenVibe's embedded JavaScript runtime.++++Excluded intentionally:++- qjs.c++- qjsc.c++- quickjs-libc.c++- quickjs-libc.h++++Reason:++OpenVibe embeds QuickJS as a sandboxed script VM. Community scripts should not++receive raw std/os/filesystem/process APIs.++DOC++++echo "[openvibe] vendored QuickJS into $DEST"++echo "[openvibe] upstream commit: $(cat "$DEST/UPSTREAM_COMMIT")"
new file mode 100755
new file mode 100755
Binary files /dev/null and b/.tmp/quickjs-smoke/smoke-quickjs differ
Binary files /dev/null and b/.tmp/quickjs-smoke/smoke-quickjs differ
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+#include <stdio.h>
+#include "quickjs.h"
+
+int main(void) {
+ JSRuntime *rt = JS_NewRuntime();
+ JSContext *ctx = JS_NewContext(rt);
+
+ JSValue value = JS_Eval(ctx, "1 + 2 + 3", 9, "<smoke>", JS_EVAL_TYPE_GLOBAL);
+ int32_t out = 0;
+ JS_ToInt32(ctx, &out, value);
+
+ JS_FreeValue(ctx, value);
+ JS_FreeContext(ctx);
+ JS_FreeRuntime(rt);
+
+ printf("quickjs result=%d\n", out);
+ return out == 6 ? 0 : 1;
+}
@@ -1,8 +1,4 @@
@@ -1,8 +1,4 @@
-import { hook } from "./core/hook.js";
-import { gamemode } from "./core/gamemode.js";
-
-globalThis.hook = hook;
-globalThis.gamemode = gamemode;
+OV.log("bridge.js loaded");
globalThis.console = {
globalThis.console = {
log: (...args) => OV.log(args.map(String).join(" ")),
log: (...args) => OV.log(args.map(String).join(" ")),
@@ -14,16 +10,11 @@ globalThis.game = {
@@ -14,16 +10,11 @@ globalThis.game = {
mode: () => OV.getMode(),
mode: () => OV.getMode(),
map: () => OV.getMapName(),
map: () => OV.getMapName(),
time: () => OV.time(),
time: () => OV.time(),
- log: (msg) => OV.log(String(msg)),
broadcast: (msg) => OV.broadcast(String(msg)),
broadcast: (msg) => OV.broadcast(String(msg)),
- runServerCommand: (cmd) => OV.serverCommand(String(cmd))
+ serverCommand: (cmd) => OV.serverCommand(String(cmd))
};
};
globalThis.round = {
globalThis.round = {
- state: () => OV.roundState(),
- setState: (state) => OV.setRoundState(String(state)),
start: () => OV.fireHook("RoundStart"),
start: () => OV.fireHook("RoundStart"),
end: () => OV.fireHook("RoundEnd")
end: () => OV.fireHook("RoundEnd")
};
};
-
-OV.log("[OpenVibe JS] bridge.js loaded");
@@ -1 +1,26 @@
@@ -1 +1,26 @@
-console.log("Base server realm loaded");
+const GM = {
+ mode: "base",
+ name: "OpenVibe Base",
+
+ Initialize() {
+ OV.log("Base Initialize fired");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source.");
+ },
+
+ PlayerSpawn(_ply) {},
+
+ PlayerDeath(_victim, _attacker) {},
+
+ PlayerDisconnected(_ply) {},
+
+ PlayerSay(_ply, _text) {
+ return undefined;
+ },
+
+ Think() {}
+};
+
+gamemode.set(GM);
@@ -1,36 +1,23 @@
@@ -1,36 +1,23 @@
-let roundState = "waiting";
-let roundEndsAt = 0;
-
-export const GM = {
+const GM = {
mode: "deathrun",
mode: "deathrun",
name: "OpenVibe Deathrun",
name: "OpenVibe Deathrun",
Initialize() {
Initialize() {
- roundState = "waiting";
+ OV.log("Deathrun Initialize fired");
},
},
PlayerInitialSpawn(ply) {
PlayerInitialSpawn(ply) {
ply.chat("Deathrun: survive the traps and reach the finish.");
ply.chat("Deathrun: survive the traps and reach the finish.");
},
},
- RoundStart() {
- roundState = "running";
- roundEndsAt = game.time() + 420;
- game.broadcast("Deathrun round started.");
- },
-
- PlayerFinish(ply) {
- ply.chat("Finished Deathrun!");
- OV.reward(ply, 50, 100, "deathrun_finish");
- return true;
- },
-
- Think() {
- if (roundState === "running" && game.time() >= roundEndsAt) {
- roundState = "ended";
- game.broadcast("Deathrun time expired.");
- OV.endMatch("time_expired");
+ PlayerSay(ply, text) {
+ if (text === "!finish") {
+ ply.chat("Deathrun finish test.");
+ OV.reward(ply, 50, 100, "deathrun_finish");
+ return false;
}
}
+
+ return undefined;
}
}
};
};
@@ -1,16 +1,11 @@
@@ -1,16 +1,11 @@
-let state = "build";
-let stateEndsAt = 0;
+const allowed = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);
-const allowedParts = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);
-
-export const GM = {
+const GM = {
mode: "fortwars",
mode: "fortwars",
name: "OpenVibe Fort Wars",
name: "OpenVibe Fort Wars",
Initialize() {
Initialize() {
- state = "build";
- stateEndsAt = game.time() + 180;
- game.broadcast("Fort Wars build phase started.");
+ OV.log("Fort Wars Initialize fired");
},
},
PlayerInitialSpawn(ply) {
PlayerInitialSpawn(ply) {
@@ -20,33 +15,14 @@ export const GM = {
@@ -20,33 +15,14 @@ export const GM = {
PlayerSay(ply, text) {
PlayerSay(ply, text) {
if (!text.startsWith("!build ")) return undefined;
if (!text.startsWith("!build ")) return undefined;
- if (state !== "build") {
- ply.chat("Build phase is over.");
- return false;
- }
-
- const part = text.slice("!build ".length).trim();
- if (!allowedParts.has(part)) {
+ const part = text.slice(7).trim();
+ if (!allowed.has(part)) {
ply.chat("Allowed: crate, barrel, pallet, fence, sheet");
ply.chat("Allowed: crate, barrel, pallet, fence, sheet");
return false;
return false;
}
}
ply.runCommand(`ov_fortwars_spawn ${part}`);
ply.runCommand(`ov_fortwars_spawn ${part}`);
return false;
return false;
- },
-
- Think() {
- if (state === "build" && game.time() >= stateEndsAt) {
- state = "fight";
- stateEndsAt = game.time() + 420;
- game.broadcast("Fort Wars fight phase started.");
- }
-
- if (state === "fight" && game.time() >= stateEndsAt) {
- state = "ended";
- game.broadcast("Fort Wars ended.");
- OV.endMatch("time_expired");
- }
}
}
};
};
@@ -1,27 +1,44 @@
@@ -1,27 +1,44 @@
-export const GM = {
+const GM = {
mode: "hub",
mode: "hub",
name: "OpenVibe Hub",
name: "OpenVibe Hub",
Initialize() {
Initialize() {
- game.broadcast("OpenVibe Hub online");
+ OV.log("Hub Initialize fired");
+ },
+
+ MapInitialize(mapName) {
+ OV.log(`Map initialized: ${mapName}`);
},
},
PlayerInitialSpawn(ply) {
PlayerInitialSpawn(ply) {
- ply.chat("Welcome to the OpenVibe Hub");
- ply.chat("Use the portals to join Prop Hunt, Deathrun, Fort Wars, or Traitor Town.");
+ ply.chat("Welcome to OpenVibe: Source JS runtime.");
+ OV.broadcast(`${ply.name()} joined the hub.`);
+ },
+
+ PlayerSpawn(ply) {
+ ply.chat("PlayerSpawn hook fired.");
},
},
- HubPortalUse(ply, mode) {
- ply.chat(`Sending you to ${mode}...`);
- ply.runCommand(`ov_join ${mode}`);
- return true;
+ PlayerSay(ply, text) {
+ if (text === "!js") {
+ ply.chat("JavaScript hooks are working.");
+ return false;
+ }
+
+ if (text === "!hp") {
+ ply.chat(`Health: ${ply.health()}`);
+ return false;
+ }
+
+ if (text === "!players") {
+ ply.chat(`Players online: ${OV.players().length}`);
+ return false;
+ }
+
+ return undefined;
},
},
- ShopNpcUse(ply, category) {
- ply.chat(`Opening ${category} shop...`);
- ply.runCommand(`ov_open_url https://openvibe.games/me/inventory`);
- return true;
- }
+ Think() {}
};
};
gamemode.set(GM);
gamemode.set(GM);
@@ -1,31 +1,21 @@
@@ -1,31 +1,21 @@
-let state = "waiting";
-let roundEndsAt = 0;
-
-const propChoices = ["can", "crate", "barrel", "chair", "bucket"];
+const props = ["crate", "barrel", "chair", "bucket"];
function randomProp() {
function randomProp() {
- return propChoices[Math.floor(Math.random() * propChoices.length)];
+ return props[Math.floor(Math.random() * props.length)];
}
}
-export const GM = {
+const GM = {
mode: "prophunt",
mode: "prophunt",
name: "OpenVibe Prop Hunt",
name: "OpenVibe Prop Hunt",
Initialize() {
Initialize() {
- state = "waiting";
- console.log("Prop Hunt initialized");
+ OV.log("Prop Hunt Initialize fired");
},
},
PlayerInitialSpawn(ply) {
PlayerInitialSpawn(ply) {
ply.chat("Prop Hunt: hide as props or hunt them down.");
ply.chat("Prop Hunt: hide as props or hunt them down.");
},
},
- PlayerSpawn(ply) {
- if (state === "hide") {
- ply.runCommand(`ov_prophunt_disguise ${randomProp()}`);
- }
- },
-
PlayerSay(ply, text) {
PlayerSay(ply, text) {
if (text === "!prop") {
if (text === "!prop") {
ply.runCommand(`ov_prophunt_disguise ${randomProp()}`);
ply.runCommand(`ov_prophunt_disguise ${randomProp()}`);
@@ -33,27 +23,14 @@ export const GM = {
@@ -33,27 +23,14 @@ export const GM = {
}
}
if (text.startsWith("!prop ")) {
if (text.startsWith("!prop ")) {
- const choice = text.slice("!prop ".length).trim();
- ply.runCommand(`ov_prophunt_disguise ${choice}`);
+ ply.runCommand(`ov_prophunt_disguise ${text.slice(6).trim()}`);
return false;
return false;
}
}
return undefined;
return undefined;
},
},
- RoundStart() {
- state = "hide";
- roundEndsAt = game.time() + 300;
- game.broadcast("Prop Hunt round started. Props hide. Hunters seek.");
- },
-
- Think() {
- if (state === "hide" && game.time() >= roundEndsAt) {
- state = "ended";
- game.broadcast("Props win!");
- OV.endMatch("props_win");
- }
- }
+ Think() {}
};
};
gamemode.set(GM);
gamemode.set(GM);
@@ -1,49 +1,29 @@
@@ -1,49 +1,29 @@
-let state = "waiting";
-let roles = new Map();
+const roles = new Map();
-function assignRoles(players) {
- roles.clear();
-
- const shuffled = [...players].sort(() => Math.random() - 0.5);
- const traitorCount = Math.max(1, Math.floor(shuffled.length / 4));
-
- for (let i = 0; i < shuffled.length; i++) {
- const role = i < traitorCount ? "traitor" : "innocent";
- roles.set(shuffled[i].steamId(), role);
- shuffled[i].chat(`Your role: ${role}`);
- }
-}
-
-export const GM = {
+const GM = {
mode: "traitortown",
mode: "traitortown",
name: "OpenVibe Traitor Town",
name: "OpenVibe Traitor Town",
Initialize() {
Initialize() {
- state = "waiting";
+ OV.log("Traitor Town Initialize fired");
},
},
PlayerInitialSpawn(ply) {
PlayerInitialSpawn(ply) {
ply.chat("Traitor Town: find the traitors before they find you.");
ply.chat("Traitor Town: find the traitors before they find you.");
- },
-
- RoundStart() {
- state = "running";
- assignRoles(OV.players());
- game.broadcast("Traitor Town round started.");
- },
-
- PlayerDeath(victim, attacker) {
- const role = roles.get(victim.steamId()) || "unknown";
- game.broadcast(`${victim.name()} died. They were ${role}.`);
+ roles.set(ply.userId(), "innocent");
},
},
PlayerSay(ply, text) {
PlayerSay(ply, text) {
if (text === "!role") {
if (text === "!role") {
- ply.chat(`Your role: ${roles.get(ply.steamId()) || "none"}`);
+ ply.chat(`Your role: ${roles.get(ply.userId()) || "none"}`);
return false;
return false;
}
}
return undefined;
return undefined;
+ },
+
+ PlayerDeath(victim, attacker) {
+ OV.broadcast(`${victim.name()} died.`);
}
}
};
};
new file mode 100644
new file mode 100644
@@ -0,0 +1,188 @@
@@ -0,0 +1,188 @@
+#include "cbase.h"
+#include "hl2mp_player.h"
+#include "openvibe_js_server.h"
+#include "openvibe/ov_js_runtime.h"
+#include "openvibe/ov_js_player.h"
+
+#include "tier0/memdbgon.h"
+
+static ConVar ov_mode(
+ "ov_mode",
+ "hub",
+ FCVAR_GAMEDLL,
+ "OpenVibe mode: hub, prophunt, deathrun, fortwars, traitortown." );
+
+static ConVar ov_js_enabled(
+ "ov_js_enabled",
+ "1",
+ FCVAR_GAMEDLL,
+ "Enable OpenVibe JavaScript runtime." );
+
+static COpenVibeJSRuntime g_OVServerJS;
+static bool g_OVServerJSStarted = false;
+
+static bool OpenVibeJS_IsRunning()
+{
+ return ov_js_enabled.GetBool() && g_OVServerJS.Context() != nullptr;
+}
+
+static void OpenVibeJS_EnsureStarted()
+{
+ if (g_OVServerJSStarted)
+ return;
+
+ g_OVServerJSStarted = true;
+
+ if (!ov_js_enabled.GetBool())
+ {
+ Msg("[OV JS] disabled by ov_js_enabled=0\n");
+ return;
+ }
+
+ if (g_OVServerJS.Init(true, ov_mode.GetString()))
+ {
+ Msg("[OV JS] server runtime initialized for mode '%s'\n", ov_mode.GetString());
+ g_OVServerJS.CallHookVoid("Initialize");
+
+ JSContext *ctx = g_OVServerJS.Context();
+ JSValue mapName = JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");
+ JSValueConst argv[] = { mapName };
+ g_OVServerJS.CallHookVoid("MapInitialize", 1, argv);
+ JS_FreeValue(ctx, mapName);
+ }
+ else
+ {
+ Warning("[OV JS] server runtime failed to initialize\n");
+ }
+}
+
+void OpenVibeJS_ServerInit()
+{
+ OpenVibeJS_EnsureStarted();
+}
+
+void OpenVibeJS_ServerShutdown()
+{
+ if (OpenVibeJS_IsRunning())
+ g_OVServerJS.CallHookVoid("Shutdown");
+
+ g_OVServerJS.Shutdown();
+ g_OVServerJSStarted = false;
+}
+
+void OpenVibeJS_ServerThink()
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning())
+ return;
+
+ g_OVServerJS.CallHookVoid("Think");
+}
+
+void OpenVibeJS_Server_PlayerInitialSpawn(CHL2MP_Player *player)
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning() || !player)
+ return;
+
+ JSContext *ctx = g_OVServerJS.Context();
+ JSValue ply = OVJS_NewPlayer(ctx, player);
+
+ JSValueConst argv[] = { ply };
+ g_OVServerJS.CallHookVoid("PlayerInitialSpawn", 1, argv);
+
+ JS_FreeValue(ctx, ply);
+}
+
+void OpenVibeJS_Server_PlayerSpawn(CHL2MP_Player *player)
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning() || !player)
+ return;
+
+ JSContext *ctx = g_OVServerJS.Context();
+ JSValue ply = OVJS_NewPlayer(ctx, player);
+
+ JSValueConst argv[] = { ply };
+ g_OVServerJS.CallHookVoid("PlayerSpawn", 1, argv);
+
+ JS_FreeValue(ctx, ply);
+}
+
+void OpenVibeJS_Server_PlayerDeath(CHL2MP_Player *victim, CBaseEntity *attacker, CBaseEntity *inflictor)
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning() || !victim)
+ return;
+
+ JSContext *ctx = g_OVServerJS.Context();
+
+ JSValue jsVictim = OVJS_NewPlayer(ctx, victim);
+ JSValue jsAttacker = JS_NULL;
+
+ if (attacker && attacker->IsPlayer())
+ jsAttacker = OVJS_NewPlayer(ctx, ToHL2MPPlayer(static_cast<CBasePlayer *>(attacker)));
+
+ JSValueConst argv[] = { jsVictim, jsAttacker };
+ g_OVServerJS.CallHookVoid("PlayerDeath", 2, argv);
+
+ JS_FreeValue(ctx, jsVictim);
+ JS_FreeValue(ctx, jsAttacker);
+}
+
+void OpenVibeJS_Server_PlayerDisconnected(CHL2MP_Player *player)
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning() || !player)
+ return;
+
+ JSContext *ctx = g_OVServerJS.Context();
+ JSValue ply = OVJS_NewPlayer(ctx, player);
+
+ JSValueConst argv[] = { ply };
+ g_OVServerJS.CallHookVoid("PlayerDisconnected", 1, argv);
+
+ JS_FreeValue(ctx, ply);
+}
+
+bool OpenVibeJS_Server_PlayerSay(CHL2MP_Player *player, const char *text)
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning() || !player || !text)
+ return false;
+
+ JSContext *ctx = g_OVServerJS.Context();
+
+ JSValue ply = OVJS_NewPlayer(ctx, player);
+ JSValue msg = JS_NewString(ctx, text);
+
+ JSValueConst argv[] = { ply, msg };
+
+ bool value = false;
+ bool returned = g_OVServerJS.CallHookBool("PlayerSay", &value, 2, argv);
+
+ JS_FreeValue(ctx, ply);
+ JS_FreeValue(ctx, msg);
+
+ return returned && value == false;
+}
+
+static void OV_JSReload_f()
+{
+ OpenVibeJS_ServerShutdown();
+ OpenVibeJS_ServerInit();
+ Msg("[OV JS] reloaded\n");
+}
+
+static ConCommand ov_js_reload(
+ "ov_js_reload",
+ OV_JSReload_f,
+ "Reload OpenVibe JavaScript runtime.",
+ FCVAR_GAMEDLL
+);
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+#pragma once
+
+class CHL2MP_Player;
+class CBasePlayer;
+class CBaseEntity;
+
+void OpenVibeJS_ServerInit();
+void OpenVibeJS_ServerShutdown();
+void OpenVibeJS_ServerThink();
+
+void OpenVibeJS_Server_PlayerInitialSpawn(CHL2MP_Player *player);
+void OpenVibeJS_Server_PlayerSpawn(CHL2MP_Player *player);
+void OpenVibeJS_Server_PlayerDeath(CHL2MP_Player *victim, CBaseEntity *attacker, CBaseEntity *inflictor);
+void OpenVibeJS_Server_PlayerDisconnected(CHL2MP_Player *player);
+bool OpenVibeJS_Server_PlayerSay(CHL2MP_Player *player, const char *text);
@@ -8,6 +8,7 @@
@@ -8,6 +8,7 @@
#include "cbase.h"
#include "cbase.h"
#include "hl2mp_player.h"
#include "hl2mp_player.h"
+#include "openvibe_js_server.h"
#include "player.h"
#include "player.h"
#include "props.h"
#include "props.h"
#include "util.h"
#include "util.h"
@@ -259,6 +260,8 @@ void OpenVibe_OnClientActive( CHL2MP_Player *pPlayer )
@@ -259,6 +260,8 @@ void OpenVibe_OnClientActive( CHL2MP_Player *pPlayer )
{
{
engine->ServerCommand( UTIL_VarArgs( "kickid %d \"OpenVibe join token required\"\n", pPlayer->GetUserID() ) );
engine->ServerCommand( UTIL_VarArgs( "kickid %d \"OpenVibe join token required\"\n", pPlayer->GetUserID() ) );
}
}
+
+ OpenVibeJS_Server_PlayerInitialSpawn( pPlayer );
}
}
static void OV_PropHuntDisguise_f( const CCommand &args )
static void OV_PropHuntDisguise_f( const CCommand &args )
@@ -789,17 +792,22 @@ static COpenVibeJsBridge g_OpenVibeJsBridge;
@@ -789,17 +792,22 @@ static COpenVibeJsBridge g_OpenVibeJsBridge;
void OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer )
void OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer )
{
{
- g_OpenVibeJsBridge.OnPlayerLeft( pPlayer );
+
+ CHL2MP_Player *pHL2MP = ToHL2MPPlayer( pPlayer );
+ if ( pHL2MP )
+ OpenVibeJS_Server_PlayerDisconnected( pHL2MP );
}
}
void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller )
void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller )
{
{
- g_OpenVibeJsBridge.OnPlayerDeath( pPlayer, pKiller );
+
+ OpenVibeJS_Server_PlayerDeath( pPlayer, pKiller, NULL );
}
}
void OpenVibe_OnFrame()
void OpenVibe_OnFrame()
{
{
- g_OpenVibeJsBridge.Update();
+
+ OpenVibeJS_ServerThink();
}
}
static void OV_JsCmd_f( const CCommand &args )
static void OV_JsCmd_f( const CCommand &args )
new file mode 100644
new file mode 100644
@@ -0,0 +1,819 @@
@@ -0,0 +1,819 @@
+// 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"
+
+// Standard library and socket includes for JS bridge
+#include <thread>
+#include <mutex>
+#include <queue>
+#include <string>
+#include <condition_variable>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sstream>
+#include <sys/select.h>
+#include <sys/time.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 );
+
+// =================================══════════════════════════════════════════
+// OpenVibe.JS - Scripting Engine C++ Bridge
+// =================================══════════════════════════════════════════
+
+class COpenVibeJsBridge
+{
+private:
+ std::thread m_workerThread;
+ std::mutex m_outgoingMutex;
+ std::queue<std::string> m_outgoingQueue;
+ std::mutex m_incomingMutex;
+ std::queue<std::string> m_incomingQueue;
+ bool m_bRunning;
+ bool m_bConnected;
+ int m_socketFd;
+ std::string m_socketPath;
+ std::string m_incomingBuffer;
+
+ void ThreadFunc()
+ {
+ while ( m_bRunning )
+ {
+ if ( !m_bConnected )
+ {
+ m_socketFd = socket( AF_UNIX, SOCK_STREAM, 0 );
+ if ( m_socketFd >= 0 )
+ {
+ struct sockaddr_un addr;
+ memset( &addr, 0, sizeof( addr ) );
+ addr.sun_family = AF_UNIX;
+ Q_strncpy( addr.sun_path, m_socketPath.c_str(), sizeof( addr.sun_path ) );
+
+ if ( connect( m_socketFd, (struct sockaddr*)&addr, sizeof( addr ) ) == 0 )
+ {
+ m_bConnected = true;
+ Msg( "[OV JS] Connected to Node.js scripting bridge at %s\n", m_socketPath.c_str() );
+ }
+ else
+ {
+ close( m_socketFd );
+ m_socketFd = -1;
+ SpawnNodeJs();
+ std::this_thread::sleep_for( std::chrono::milliseconds( 2000 ) );
+ continue;
+ }
+ }
+ }
+
+ if ( m_bConnected )
+ {
+ fd_set readFds;
+ fd_set writeFds;
+ FD_ZERO( &readFds );
+ FD_ZERO( &writeFds );
+ FD_SET( m_socketFd, &readFds );
+
+ bool bHasWrite = false;
+ {
+ std::lock_guard<std::mutex> lock( m_outgoingMutex );
+ bHasWrite = !m_outgoingQueue.empty();
+ }
+ if ( bHasWrite )
+ {
+ FD_SET( m_socketFd, &writeFds );
+ }
+
+ struct timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 50000;
+
+ int activity = select( m_socketFd + 1, &readFds, &writeFds, NULL, &tv );
+ if ( activity < 0 )
+ {
+ Disconnect();
+ continue;
+ }
+
+ if ( FD_ISSET( m_socketFd, &readFds ) )
+ {
+ char buffer[4096];
+ int bytesRead = recv( m_socketFd, buffer, sizeof( buffer ) - 1, 0 );
+ if ( bytesRead <= 0 )
+ {
+ Disconnect();
+ continue;
+ }
+ buffer[bytesRead] = '\0';
+ ProcessIncomingBuffer( buffer );
+ }
+
+ if ( bHasWrite && FD_ISSET( m_socketFd, &writeFds ) )
+ {
+ std::string msg;
+ {
+ std::lock_guard<std::mutex> lock( m_outgoingMutex );
+ if ( !m_outgoingQueue.empty() )
+ {
+ msg = m_outgoingQueue.front();
+ m_outgoingQueue.pop();
+ }
+ }
+ if ( !msg.empty() )
+ {
+ int sent = send( m_socketFd, msg.c_str(), msg.length(), 0 );
+ if ( sent < 0 )
+ {
+ Disconnect();
+ continue;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void SpawnNodeJs()
+ {
+ static float lastSpawnTime = 0.0f;
+ float curTime = gpGlobals ? gpGlobals->realtime : 0.0f;
+ if ( curTime > 0.0f && curTime - lastSpawnTime < 10.0f )
+ {
+ return;
+ }
+ lastSpawnTime = curTime;
+
+ Msg( "[OV JS] Starting Node.js OpenVibe.JS runtime...\n" );
+ char cmd[1024];
+ const char *root = getenv( "OPENVIBE_ROOT" );
+ if ( !root )
+ {
+ root = "/home/workstation/src/openvibe-source";
+ }
+ Q_snprintf( cmd, sizeof( cmd ), "node %s/engine/openvibe-js-runtime/bridge.js %s > /tmp/openvibe-js-%s.log 2>&1 &",
+ root, m_socketPath.c_str(), ov_server_id.GetString() );
+ int ret = system( cmd );
+ (void)ret;
+ }
+
+ void Disconnect()
+ {
+ if ( m_socketFd >= 0 )
+ {
+ close( m_socketFd );
+ m_socketFd = -1;
+ }
+ m_bConnected = false;
+ Msg( "[OV JS] Disconnected from Node.js scripting bridge.\n" );
+ }
+
+ void ProcessIncomingBuffer( const char *data )
+ {
+ m_incomingBuffer += data;
+ size_t pos;
+ while ( ( pos = m_incomingBuffer.find( '\n' ) ) != std::string::npos )
+ {
+ std::string line = m_incomingBuffer.substr( 0, pos );
+ m_incomingBuffer.erase( 0, pos + 1 );
+ if ( !line.empty() )
+ {
+ std::lock_guard<std::mutex> lock( m_incomingMutex );
+ m_incomingQueue.push( line );
+ }
+ }
+ }
+
+public:
+ COpenVibeJsBridge() :
+ m_bRunning( false ),
+ m_bConnected( false ),
+ m_socketFd( -1 )
+ {
+ }
+
+ ~COpenVibeJsBridge()
+ {
+ Stop();
+ }
+
+ void Start()
+ {
+ if ( m_bRunning ) return;
+
+ char szSocket[256];
+ Q_snprintf( szSocket, sizeof( szSocket ), "/tmp/openvibe-js-%s.sock", ov_server_id.GetString() );
+ m_socketPath = szSocket;
+
+ m_bRunning = true;
+ m_workerThread = std::thread( &COpenVibeJsBridge::ThreadFunc, this );
+ Msg( "[OV JS] Scripting bridge service thread started. socket=%s\n", m_socketPath.c_str() );
+ }
+
+ void Stop()
+ {
+ m_bRunning = false;
+ if ( m_workerThread.joinable() )
+ {
+ m_workerThread.join();
+ }
+ Disconnect();
+ }
+
+ void SendMessage( const std::string &msg )
+ {
+ if ( !m_bConnected ) return;
+ std::lock_guard<std::mutex> lock( m_outgoingMutex );
+ m_outgoingQueue.push( msg + "\n" );
+ }
+
+ void OnPlayerJoined( CBasePlayer *pPlayer )
+ {
+ if ( !pPlayer ) return;
+
+ char szSteamID[32];
+ CSteamID steamID;
+ if ( pPlayer->GetSteamID( &steamID ) )
+ {
+ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );
+ }
+ else
+ {
+ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );
+ }
+
+ std::stringstream ss;
+ ss << "{\"type\":\"player_join\",\"data\":{"
+ << "\"steamId\":\"" << szSteamID << "\","
+ << "\"name\":\"" << pPlayer->GetPlayerName() << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void OnPlayerLeft( CBasePlayer *pPlayer )
+ {
+ if ( !pPlayer ) return;
+
+ char szSteamID[32];
+ CSteamID steamID;
+ if ( pPlayer->GetSteamID( &steamID ) )
+ {
+ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );
+ }
+ else
+ {
+ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );
+ }
+
+ std::stringstream ss;
+ ss << "{\"type\":\"player_leave\",\"data\":{"
+ << "\"steamId\":\"" << szSteamID << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void OnPlayerDeath( CBasePlayer *pPlayer, CBaseEntity *pKiller )
+ {
+ if ( !pPlayer ) return;
+
+ char szDeadSteamID[32];
+ CSteamID deadSteamID;
+ if ( pPlayer->GetSteamID( &deadSteamID ) )
+ {
+ Q_snprintf( szDeadSteamID, sizeof( szDeadSteamID ), "%llu", deadSteamID.ConvertToUint64() );
+ }
+ else
+ {
+ Q_strncpy( szDeadSteamID, "0", sizeof( szDeadSteamID ) );
+ }
+
+ char szKillerSteamID[32] = "0";
+ if ( pKiller && pKiller->IsPlayer() )
+ {
+ CBasePlayer *pKillerPlayer = (CBasePlayer*)pKiller;
+ CSteamID killerSteamID;
+ if ( pKillerPlayer->GetSteamID( &killerSteamID ) )
+ {
+ Q_snprintf( szKillerSteamID, sizeof( szKillerSteamID ), "%llu", killerSteamID.ConvertToUint64() );
+ }
+ }
+
+ std::stringstream ss;
+ ss << "{\"type\":\"player_death\",\"data\":{"
+ << "\"deadSteamId\":\"" << szDeadSteamID << "\","
+ << "\"killerSteamId\":\"" << szKillerSteamID << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void OnCommand( const char *cmd )
+ {
+ std::stringstream ss;
+ ss << "{\"type\":\"command\",\"data\":{"
+ << "\"cmd\":\"" << cmd << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void Update()
+ {
+ if ( !m_bRunning )
+ {
+ Start();
+ }
+
+ static float lastTickTime = 0.0f;
+ float curTime = gpGlobals->curtime;
+ if ( curTime - lastTickTime >= 1.0f )
+ {
+ lastTickTime = curTime;
+ SendMessage( "{\"type\":\"tick\",\"data\":{}}" );
+ }
+
+ std::queue<std::string> localQueue;
+ {
+ std::lock_guard<std::mutex> lock( m_incomingMutex );
+ while ( !m_incomingQueue.empty() )
+ {
+ localQueue.push( m_incomingQueue.front() );
+ m_incomingQueue.pop();
+ }
+ }
+
+ while ( !localQueue.empty() )
+ {
+ std::string line = localQueue.front();
+ localQueue.pop();
+ ProcessMessage( line );
+ }
+ }
+
+private:
+ void ProcessMessage( const std::string &line )
+ {
+ if ( line.find( "\"type\":\"broadcast\"" ) != std::string::npos )
+ {
+ size_t msgPos = line.find( "\"message\":\"" );
+ if ( msgPos != std::string::npos )
+ {
+ msgPos += 11;
+ size_t endPos = line.find( "\"", msgPos );
+ if ( endPos != std::string::npos )
+ {
+ std::string msg = line.substr( msgPos, endPos - msgPos );
+ UTIL_ClientPrintAll( HUD_PRINTTALK, "[OV JS] %s", msg.c_str() );
+ }
+ }
+ }
+ else if ( line.find( "\"type\":\"entity_spawn\"" ) != std::string::npos )
+ {
+ std::string type = "prop_physics_override";
+ std::string model = "";
+ Vector pos( 0, 0, 0 );
+
+ size_t tPos = line.find( "\"type\":\"" );
+ if ( tPos != std::string::npos )
+ {
+ size_t dataPos = line.find( "\"data\":{" );
+ if ( dataPos != std::string::npos )
+ {
+ size_t innerType = line.find( "\"type\":\"", dataPos );
+ if ( innerType != std::string::npos )
+ {
+ innerType += 8;
+ size_t end = line.find( "\"", innerType );
+ if ( end != std::string::npos )
+ {
+ type = line.substr( innerType, end - innerType );
+ }
+ }
+ size_t innerModel = line.find( "\"model\":\"", dataPos );
+ if ( innerModel != std::string::npos )
+ {
+ innerModel += 9;
+ size_t end = line.find( "\"", innerModel );
+ if ( end != std::string::npos )
+ {
+ model = line.substr( innerModel, end - innerModel );
+ }
+ }
+ size_t xPos = line.find( "\"x\":", dataPos );
+ size_t yPos = line.find( "\"y\":", dataPos );
+ size_t zPos = line.find( "\"z\":", dataPos );
+ if ( xPos != std::string::npos && yPos != std::string::npos && zPos != std::string::npos )
+ {
+ pos.x = atof( line.c_str() + xPos + 4 );
+ pos.y = atof( line.c_str() + yPos + 4 );
+ pos.z = atof( line.c_str() + zPos + 4 );
+ }
+ }
+ }
+
+ if ( !model.empty() )
+ {
+ CBaseEntity::PrecacheModel( model.c_str() );
+ CBaseEntity *pEnt = CreateEntityByName( type.c_str() );
+ if ( pEnt )
+ {
+ pEnt->KeyValue( "model", model.c_str() );
+ pEnt->SetAbsOrigin( pos );
+ DispatchSpawn( pEnt );
+ pEnt->Activate();
+ Msg( "[OV JS] Spawned entity %s with model %s at (%f, %f, %f)\n",
+ type.c_str(), model.c_str(), pos.x, pos.y, pos.z );
+ }
+ }
+ }
+ }
+};
+
+static COpenVibeJsBridge g_OpenVibeJsBridge;
+
+void OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer )
+{
+ g_OpenVibeJsBridge.OnPlayerLeft( pPlayer );
+}
+
+void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller )
+{
+ g_OpenVibeJsBridge.OnPlayerDeath( pPlayer, pKiller );
+}
+
+void OpenVibe_OnFrame()
+{
+ g_OpenVibeJsBridge.Update();
+}
+
+static void OV_JsCmd_f( const CCommand &args )
+{
+ if ( args.ArgC() < 2 )
+ {
+ Msg( "Usage: ov_js_cmd <command_or_json>\n" );
+ return;
+ }
+ g_OpenVibeJsBridge.OnCommand( args.ArgS() );
+}
+
+static ConCommand ov_js_cmd(
+ "ov_js_cmd",
+ OV_JsCmd_f,
+ "Send a command or JSON message to the OpenVibe.JS runtime.",
+ FCVAR_GAMEDLL );
new file mode 100644
new file mode 100644
@@ -0,0 +1,827 @@
@@ -0,0 +1,827 @@
+// 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 "openvibe_js_server.h"
+#include "player.h"
+#include "props.h"
+#include "util.h"
+
+#include "steam/steam_gameserver.h"
+#include "steam/isteamhttp.h"
+
+// Standard library and socket includes for JS bridge
+#include <thread>
+#include <mutex>
+#include <queue>
+#include <string>
+#include <condition_variable>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sstream>
+#include <sys/select.h>
+#include <sys/time.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() ) );
+ }
+
+ OpenVibeJS_Server_PlayerInitialSpawn( pPlayer );
+}
+
+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 );
+
+// =================================══════════════════════════════════════════
+// OpenVibe.JS - Scripting Engine C++ Bridge
+// =================================══════════════════════════════════════════
+
+class COpenVibeJsBridge
+{
+private:
+ std::thread m_workerThread;
+ std::mutex m_outgoingMutex;
+ std::queue<std::string> m_outgoingQueue;
+ std::mutex m_incomingMutex;
+ std::queue<std::string> m_incomingQueue;
+ bool m_bRunning;
+ bool m_bConnected;
+ int m_socketFd;
+ std::string m_socketPath;
+ std::string m_incomingBuffer;
+
+ void ThreadFunc()
+ {
+ while ( m_bRunning )
+ {
+ if ( !m_bConnected )
+ {
+ m_socketFd = socket( AF_UNIX, SOCK_STREAM, 0 );
+ if ( m_socketFd >= 0 )
+ {
+ struct sockaddr_un addr;
+ memset( &addr, 0, sizeof( addr ) );
+ addr.sun_family = AF_UNIX;
+ Q_strncpy( addr.sun_path, m_socketPath.c_str(), sizeof( addr.sun_path ) );
+
+ if ( connect( m_socketFd, (struct sockaddr*)&addr, sizeof( addr ) ) == 0 )
+ {
+ m_bConnected = true;
+ Msg( "[OV JS] Connected to Node.js scripting bridge at %s\n", m_socketPath.c_str() );
+ }
+ else
+ {
+ close( m_socketFd );
+ m_socketFd = -1;
+ SpawnNodeJs();
+ std::this_thread::sleep_for( std::chrono::milliseconds( 2000 ) );
+ continue;
+ }
+ }
+ }
+
+ if ( m_bConnected )
+ {
+ fd_set readFds;
+ fd_set writeFds;
+ FD_ZERO( &readFds );
+ FD_ZERO( &writeFds );
+ FD_SET( m_socketFd, &readFds );
+
+ bool bHasWrite = false;
+ {
+ std::lock_guard<std::mutex> lock( m_outgoingMutex );
+ bHasWrite = !m_outgoingQueue.empty();
+ }
+ if ( bHasWrite )
+ {
+ FD_SET( m_socketFd, &writeFds );
+ }
+
+ struct timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 50000;
+
+ int activity = select( m_socketFd + 1, &readFds, &writeFds, NULL, &tv );
+ if ( activity < 0 )
+ {
+ Disconnect();
+ continue;
+ }
+
+ if ( FD_ISSET( m_socketFd, &readFds ) )
+ {
+ char buffer[4096];
+ int bytesRead = recv( m_socketFd, buffer, sizeof( buffer ) - 1, 0 );
+ if ( bytesRead <= 0 )
+ {
+ Disconnect();
+ continue;
+ }
+ buffer[bytesRead] = '\0';
+ ProcessIncomingBuffer( buffer );
+ }
+
+ if ( bHasWrite && FD_ISSET( m_socketFd, &writeFds ) )
+ {
+ std::string msg;
+ {
+ std::lock_guard<std::mutex> lock( m_outgoingMutex );
+ if ( !m_outgoingQueue.empty() )
+ {
+ msg = m_outgoingQueue.front();
+ m_outgoingQueue.pop();
+ }
+ }
+ if ( !msg.empty() )
+ {
+ int sent = send( m_socketFd, msg.c_str(), msg.length(), 0 );
+ if ( sent < 0 )
+ {
+ Disconnect();
+ continue;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void SpawnNodeJs()
+ {
+ static float lastSpawnTime = 0.0f;
+ float curTime = gpGlobals ? gpGlobals->realtime : 0.0f;
+ if ( curTime > 0.0f && curTime - lastSpawnTime < 10.0f )
+ {
+ return;
+ }
+ lastSpawnTime = curTime;
+
+ Msg( "[OV JS] Starting Node.js OpenVibe.JS runtime...\n" );
+ char cmd[1024];
+ const char *root = getenv( "OPENVIBE_ROOT" );
+ if ( !root )
+ {
+ root = "/home/workstation/src/openvibe-source";
+ }
+ Q_snprintf( cmd, sizeof( cmd ), "node %s/engine/openvibe-js-runtime/bridge.js %s > /tmp/openvibe-js-%s.log 2>&1 &",
+ root, m_socketPath.c_str(), ov_server_id.GetString() );
+ int ret = system( cmd );
+ (void)ret;
+ }
+
+ void Disconnect()
+ {
+ if ( m_socketFd >= 0 )
+ {
+ close( m_socketFd );
+ m_socketFd = -1;
+ }
+ m_bConnected = false;
+ Msg( "[OV JS] Disconnected from Node.js scripting bridge.\n" );
+ }
+
+ void ProcessIncomingBuffer( const char *data )
+ {
+ m_incomingBuffer += data;
+ size_t pos;
+ while ( ( pos = m_incomingBuffer.find( '\n' ) ) != std::string::npos )
+ {
+ std::string line = m_incomingBuffer.substr( 0, pos );
+ m_incomingBuffer.erase( 0, pos + 1 );
+ if ( !line.empty() )
+ {
+ std::lock_guard<std::mutex> lock( m_incomingMutex );
+ m_incomingQueue.push( line );
+ }
+ }
+ }
+
+public:
+ COpenVibeJsBridge() :
+ m_bRunning( false ),
+ m_bConnected( false ),
+ m_socketFd( -1 )
+ {
+ }
+
+ ~COpenVibeJsBridge()
+ {
+ Stop();
+ }
+
+ void Start()
+ {
+ if ( m_bRunning ) return;
+
+ char szSocket[256];
+ Q_snprintf( szSocket, sizeof( szSocket ), "/tmp/openvibe-js-%s.sock", ov_server_id.GetString() );
+ m_socketPath = szSocket;
+
+ m_bRunning = true;
+ m_workerThread = std::thread( &COpenVibeJsBridge::ThreadFunc, this );
+ Msg( "[OV JS] Scripting bridge service thread started. socket=%s\n", m_socketPath.c_str() );
+ }
+
+ void Stop()
+ {
+ m_bRunning = false;
+ if ( m_workerThread.joinable() )
+ {
+ m_workerThread.join();
+ }
+ Disconnect();
+ }
+
+ void SendMessage( const std::string &msg )
+ {
+ if ( !m_bConnected ) return;
+ std::lock_guard<std::mutex> lock( m_outgoingMutex );
+ m_outgoingQueue.push( msg + "\n" );
+ }
+
+ void OnPlayerJoined( CBasePlayer *pPlayer )
+ {
+ if ( !pPlayer ) return;
+
+ char szSteamID[32];
+ CSteamID steamID;
+ if ( pPlayer->GetSteamID( &steamID ) )
+ {
+ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );
+ }
+ else
+ {
+ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );
+ }
+
+ std::stringstream ss;
+ ss << "{\"type\":\"player_join\",\"data\":{"
+ << "\"steamId\":\"" << szSteamID << "\","
+ << "\"name\":\"" << pPlayer->GetPlayerName() << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void OnPlayerLeft( CBasePlayer *pPlayer )
+ {
+ if ( !pPlayer ) return;
+
+ char szSteamID[32];
+ CSteamID steamID;
+ if ( pPlayer->GetSteamID( &steamID ) )
+ {
+ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );
+ }
+ else
+ {
+ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );
+ }
+
+ std::stringstream ss;
+ ss << "{\"type\":\"player_leave\",\"data\":{"
+ << "\"steamId\":\"" << szSteamID << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void OnPlayerDeath( CBasePlayer *pPlayer, CBaseEntity *pKiller )
+ {
+ if ( !pPlayer ) return;
+
+ char szDeadSteamID[32];
+ CSteamID deadSteamID;
+ if ( pPlayer->GetSteamID( &deadSteamID ) )
+ {
+ Q_snprintf( szDeadSteamID, sizeof( szDeadSteamID ), "%llu", deadSteamID.ConvertToUint64() );
+ }
+ else
+ {
+ Q_strncpy( szDeadSteamID, "0", sizeof( szDeadSteamID ) );
+ }
+
+ char szKillerSteamID[32] = "0";
+ if ( pKiller && pKiller->IsPlayer() )
+ {
+ CBasePlayer *pKillerPlayer = (CBasePlayer*)pKiller;
+ CSteamID killerSteamID;
+ if ( pKillerPlayer->GetSteamID( &killerSteamID ) )
+ {
+ Q_snprintf( szKillerSteamID, sizeof( szKillerSteamID ), "%llu", killerSteamID.ConvertToUint64() );
+ }
+ }
+
+ std::stringstream ss;
+ ss << "{\"type\":\"player_death\",\"data\":{"
+ << "\"deadSteamId\":\"" << szDeadSteamID << "\","
+ << "\"killerSteamId\":\"" << szKillerSteamID << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void OnCommand( const char *cmd )
+ {
+ std::stringstream ss;
+ ss << "{\"type\":\"command\",\"data\":{"
+ << "\"cmd\":\"" << cmd << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void Update()
+ {
+ if ( !m_bRunning )
+ {
+ Start();
+ }
+
+ static float lastTickTime = 0.0f;
+ float curTime = gpGlobals->curtime;
+ if ( curTime - lastTickTime >= 1.0f )
+ {
+ lastTickTime = curTime;
+ SendMessage( "{\"type\":\"tick\",\"data\":{}}" );
+ }
+
+ std::queue<std::string> localQueue;
+ {
+ std::lock_guard<std::mutex> lock( m_incomingMutex );
+ while ( !m_incomingQueue.empty() )
+ {
+ localQueue.push( m_incomingQueue.front() );
+ m_incomingQueue.pop();
+ }
+ }
+
+ while ( !localQueue.empty() )
+ {
+ std::string line = localQueue.front();
+ localQueue.pop();
+ ProcessMessage( line );
+ }
+ }
+
+private:
+ void ProcessMessage( const std::string &line )
+ {
+ if ( line.find( "\"type\":\"broadcast\"" ) != std::string::npos )
+ {
+ size_t msgPos = line.find( "\"message\":\"" );
+ if ( msgPos != std::string::npos )
+ {
+ msgPos += 11;
+ size_t endPos = line.find( "\"", msgPos );
+ if ( endPos != std::string::npos )
+ {
+ std::string msg = line.substr( msgPos, endPos - msgPos );
+ UTIL_ClientPrintAll( HUD_PRINTTALK, "[OV JS] %s", msg.c_str() );
+ }
+ }
+ }
+ else if ( line.find( "\"type\":\"entity_spawn\"" ) != std::string::npos )
+ {
+ std::string type = "prop_physics_override";
+ std::string model = "";
+ Vector pos( 0, 0, 0 );
+
+ size_t tPos = line.find( "\"type\":\"" );
+ if ( tPos != std::string::npos )
+ {
+ size_t dataPos = line.find( "\"data\":{" );
+ if ( dataPos != std::string::npos )
+ {
+ size_t innerType = line.find( "\"type\":\"", dataPos );
+ if ( innerType != std::string::npos )
+ {
+ innerType += 8;
+ size_t end = line.find( "\"", innerType );
+ if ( end != std::string::npos )
+ {
+ type = line.substr( innerType, end - innerType );
+ }
+ }
+ size_t innerModel = line.find( "\"model\":\"", dataPos );
+ if ( innerModel != std::string::npos )
+ {
+ innerModel += 9;
+ size_t end = line.find( "\"", innerModel );
+ if ( end != std::string::npos )
+ {
+ model = line.substr( innerModel, end - innerModel );
+ }
+ }
+ size_t xPos = line.find( "\"x\":", dataPos );
+ size_t yPos = line.find( "\"y\":", dataPos );
+ size_t zPos = line.find( "\"z\":", dataPos );
+ if ( xPos != std::string::npos && yPos != std::string::npos && zPos != std::string::npos )
+ {
+ pos.x = atof( line.c_str() + xPos + 4 );
+ pos.y = atof( line.c_str() + yPos + 4 );
+ pos.z = atof( line.c_str() + zPos + 4 );
+ }
+ }
+ }
+
+ if ( !model.empty() )
+ {
+ CBaseEntity::PrecacheModel( model.c_str() );
+ CBaseEntity *pEnt = CreateEntityByName( type.c_str() );
+ if ( pEnt )
+ {
+ pEnt->KeyValue( "model", model.c_str() );
+ pEnt->SetAbsOrigin( pos );
+ DispatchSpawn( pEnt );
+ pEnt->Activate();
+ Msg( "[OV JS] Spawned entity %s with model %s at (%f, %f, %f)\n",
+ type.c_str(), model.c_str(), pos.x, pos.y, pos.z );
+ }
+ }
+ }
+ }
+};
+
+static COpenVibeJsBridge g_OpenVibeJsBridge;
+
+void OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer )
+{
+
+ CHL2MP_Player *pHL2MP = ToHL2MPPlayer( pPlayer );
+ if ( pHL2MP )
+ OpenVibeJS_Server_PlayerDisconnected( pHL2MP );
+}
+
+void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller )
+{
+
+ OpenVibeJS_Server_PlayerDeath( pPlayer, pKiller, NULL );
+}
+
+void OpenVibe_OnFrame()
+{
+
+ OpenVibeJS_ServerThink();
+}
+
+static void OV_JsCmd_f( const CCommand &args )
+{
+ if ( args.ArgC() < 2 )
+ {
+ Msg( "Usage: ov_js_cmd <command_or_json>\n" );
+ return;
+ }
+ g_OpenVibeJsBridge.OnCommand( args.ArgS() );
+}
+
+static ConCommand ov_js_cmd(
+ "ov_js_cmd",
+ OV_JsCmd_f,
+ "Send a command or JSON message to the OpenVibe.JS runtime.",
+ FCVAR_GAMEDLL );
new file mode 100644
new file mode 100644
@@ -0,0 +1,827 @@
@@ -0,0 +1,827 @@
+// 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 "openvibe_js_server.h"
+#include "player.h"
+#include "props.h"
+#include "util.h"
+
+#include "steam/steam_gameserver.h"
+#include "steam/isteamhttp.h"
+
+// Standard library and socket includes for JS bridge
+#include <thread>
+#include <mutex>
+#include <queue>
+#include <string>
+#include <condition_variable>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sstream>
+#include <sys/select.h>
+#include <sys/time.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() ) );
+ }
+
+ OpenVibeJS_Server_PlayerInitialSpawn( pPlayer );
+}
+
+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 );
+
+// =================================══════════════════════════════════════════
+// OpenVibe.JS - Scripting Engine C++ Bridge
+// =================================══════════════════════════════════════════
+
+class COpenVibeJsBridge
+{
+private:
+ std::thread m_workerThread;
+ std::mutex m_outgoingMutex;
+ std::queue<std::string> m_outgoingQueue;
+ std::mutex m_incomingMutex;
+ std::queue<std::string> m_incomingQueue;
+ bool m_bRunning;
+ bool m_bConnected;
+ int m_socketFd;
+ std::string m_socketPath;
+ std::string m_incomingBuffer;
+
+ void ThreadFunc()
+ {
+ while ( m_bRunning )
+ {
+ if ( !m_bConnected )
+ {
+ m_socketFd = socket( AF_UNIX, SOCK_STREAM, 0 );
+ if ( m_socketFd >= 0 )
+ {
+ struct sockaddr_un addr;
+ memset( &addr, 0, sizeof( addr ) );
+ addr.sun_family = AF_UNIX;
+ Q_strncpy( addr.sun_path, m_socketPath.c_str(), sizeof( addr.sun_path ) );
+
+ if ( connect( m_socketFd, (struct sockaddr*)&addr, sizeof( addr ) ) == 0 )
+ {
+ m_bConnected = true;
+ Msg( "[OV JS] Connected to Node.js scripting bridge at %s\n", m_socketPath.c_str() );
+ }
+ else
+ {
+ close( m_socketFd );
+ m_socketFd = -1;
+ SpawnNodeJs();
+ std::this_thread::sleep_for( std::chrono::milliseconds( 2000 ) );
+ continue;
+ }
+ }
+ }
+
+ if ( m_bConnected )
+ {
+ fd_set readFds;
+ fd_set writeFds;
+ FD_ZERO( &readFds );
+ FD_ZERO( &writeFds );
+ FD_SET( m_socketFd, &readFds );
+
+ bool bHasWrite = false;
+ {
+ std::lock_guard<std::mutex> lock( m_outgoingMutex );
+ bHasWrite = !m_outgoingQueue.empty();
+ }
+ if ( bHasWrite )
+ {
+ FD_SET( m_socketFd, &writeFds );
+ }
+
+ struct timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 50000;
+
+ int activity = select( m_socketFd + 1, &readFds, &writeFds, NULL, &tv );
+ if ( activity < 0 )
+ {
+ Disconnect();
+ continue;
+ }
+
+ if ( FD_ISSET( m_socketFd, &readFds ) )
+ {
+ char buffer[4096];
+ int bytesRead = recv( m_socketFd, buffer, sizeof( buffer ) - 1, 0 );
+ if ( bytesRead <= 0 )
+ {
+ Disconnect();
+ continue;
+ }
+ buffer[bytesRead] = '\0';
+ ProcessIncomingBuffer( buffer );
+ }
+
+ if ( bHasWrite && FD_ISSET( m_socketFd, &writeFds ) )
+ {
+ std::string msg;
+ {
+ std::lock_guard<std::mutex> lock( m_outgoingMutex );
+ if ( !m_outgoingQueue.empty() )
+ {
+ msg = m_outgoingQueue.front();
+ m_outgoingQueue.pop();
+ }
+ }
+ if ( !msg.empty() )
+ {
+ int sent = send( m_socketFd, msg.c_str(), msg.length(), 0 );
+ if ( sent < 0 )
+ {
+ Disconnect();
+ continue;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void SpawnNodeJs()
+ {
+ static float lastSpawnTime = 0.0f;
+ float curTime = gpGlobals ? gpGlobals->realtime : 0.0f;
+ if ( curTime > 0.0f && curTime - lastSpawnTime < 10.0f )
+ {
+ return;
+ }
+ lastSpawnTime = curTime;
+
+ Msg( "[OV JS] Starting Node.js OpenVibe.JS runtime...\n" );
+ char cmd[1024];
+ const char *root = getenv( "OPENVIBE_ROOT" );
+ if ( !root )
+ {
+ root = "/home/workstation/src/openvibe-source";
+ }
+ Q_snprintf( cmd, sizeof( cmd ), "node %s/engine/openvibe-js-runtime/bridge.js %s > /tmp/openvibe-js-%s.log 2>&1 &",
+ root, m_socketPath.c_str(), ov_server_id.GetString() );
+ int ret = system( cmd );
+ (void)ret;
+ }
+
+ void Disconnect()
+ {
+ if ( m_socketFd >= 0 )
+ {
+ close( m_socketFd );
+ m_socketFd = -1;
+ }
+ m_bConnected = false;
+ Msg( "[OV JS] Disconnected from Node.js scripting bridge.\n" );
+ }
+
+ void ProcessIncomingBuffer( const char *data )
+ {
+ m_incomingBuffer += data;
+ size_t pos;
+ while ( ( pos = m_incomingBuffer.find( '\n' ) ) != std::string::npos )
+ {
+ std::string line = m_incomingBuffer.substr( 0, pos );
+ m_incomingBuffer.erase( 0, pos + 1 );
+ if ( !line.empty() )
+ {
+ std::lock_guard<std::mutex> lock( m_incomingMutex );
+ m_incomingQueue.push( line );
+ }
+ }
+ }
+
+public:
+ COpenVibeJsBridge() :
+ m_bRunning( false ),
+ m_bConnected( false ),
+ m_socketFd( -1 )
+ {
+ }
+
+ ~COpenVibeJsBridge()
+ {
+ Stop();
+ }
+
+ void Start()
+ {
+ if ( m_bRunning ) return;
+
+ char szSocket[256];
+ Q_snprintf( szSocket, sizeof( szSocket ), "/tmp/openvibe-js-%s.sock", ov_server_id.GetString() );
+ m_socketPath = szSocket;
+
+ m_bRunning = true;
+ m_workerThread = std::thread( &COpenVibeJsBridge::ThreadFunc, this );
+ Msg( "[OV JS] Scripting bridge service thread started. socket=%s\n", m_socketPath.c_str() );
+ }
+
+ void Stop()
+ {
+ m_bRunning = false;
+ if ( m_workerThread.joinable() )
+ {
+ m_workerThread.join();
+ }
+ Disconnect();
+ }
+
+ void SendMessage( const std::string &msg )
+ {
+ if ( !m_bConnected ) return;
+ std::lock_guard<std::mutex> lock( m_outgoingMutex );
+ m_outgoingQueue.push( msg + "\n" );
+ }
+
+ void OnPlayerJoined( CBasePlayer *pPlayer )
+ {
+ if ( !pPlayer ) return;
+
+ char szSteamID[32];
+ CSteamID steamID;
+ if ( pPlayer->GetSteamID( &steamID ) )
+ {
+ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );
+ }
+ else
+ {
+ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );
+ }
+
+ std::stringstream ss;
+ ss << "{\"type\":\"player_join\",\"data\":{"
+ << "\"steamId\":\"" << szSteamID << "\","
+ << "\"name\":\"" << pPlayer->GetPlayerName() << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void OnPlayerLeft( CBasePlayer *pPlayer )
+ {
+ if ( !pPlayer ) return;
+
+ char szSteamID[32];
+ CSteamID steamID;
+ if ( pPlayer->GetSteamID( &steamID ) )
+ {
+ Q_snprintf( szSteamID, sizeof( szSteamID ), "%llu", steamID.ConvertToUint64() );
+ }
+ else
+ {
+ Q_strncpy( szSteamID, "0", sizeof( szSteamID ) );
+ }
+
+ std::stringstream ss;
+ ss << "{\"type\":\"player_leave\",\"data\":{"
+ << "\"steamId\":\"" << szSteamID << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void OnPlayerDeath( CBasePlayer *pPlayer, CBaseEntity *pKiller )
+ {
+ if ( !pPlayer ) return;
+
+ char szDeadSteamID[32];
+ CSteamID deadSteamID;
+ if ( pPlayer->GetSteamID( &deadSteamID ) )
+ {
+ Q_snprintf( szDeadSteamID, sizeof( szDeadSteamID ), "%llu", deadSteamID.ConvertToUint64() );
+ }
+ else
+ {
+ Q_strncpy( szDeadSteamID, "0", sizeof( szDeadSteamID ) );
+ }
+
+ char szKillerSteamID[32] = "0";
+ if ( pKiller && pKiller->IsPlayer() )
+ {
+ CBasePlayer *pKillerPlayer = (CBasePlayer*)pKiller;
+ CSteamID killerSteamID;
+ if ( pKillerPlayer->GetSteamID( &killerSteamID ) )
+ {
+ Q_snprintf( szKillerSteamID, sizeof( szKillerSteamID ), "%llu", killerSteamID.ConvertToUint64() );
+ }
+ }
+
+ std::stringstream ss;
+ ss << "{\"type\":\"player_death\",\"data\":{"
+ << "\"deadSteamId\":\"" << szDeadSteamID << "\","
+ << "\"killerSteamId\":\"" << szKillerSteamID << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void OnCommand( const char *cmd )
+ {
+ std::stringstream ss;
+ ss << "{\"type\":\"command\",\"data\":{"
+ << "\"cmd\":\"" << cmd << "\""
+ << "}}";
+ SendMessage( ss.str() );
+ }
+
+ void Update()
+ {
+ if ( !m_bRunning )
+ {
+ Start();
+ }
+
+ static float lastTickTime = 0.0f;
+ float curTime = gpGlobals->curtime;
+ if ( curTime - lastTickTime >= 1.0f )
+ {
+ lastTickTime = curTime;
+ SendMessage( "{\"type\":\"tick\",\"data\":{}}" );
+ }
+
+ std::queue<std::string> localQueue;
+ {
+ std::lock_guard<std::mutex> lock( m_incomingMutex );
+ while ( !m_incomingQueue.empty() )
+ {
+ localQueue.push( m_incomingQueue.front() );
+ m_incomingQueue.pop();
+ }
+ }
+
+ while ( !localQueue.empty() )
+ {
+ std::string line = localQueue.front();
+ localQueue.pop();
+ ProcessMessage( line );
+ }
+ }
+
+private:
+ void ProcessMessage( const std::string &line )
+ {
+ if ( line.find( "\"type\":\"broadcast\"" ) != std::string::npos )
+ {
+ size_t msgPos = line.find( "\"message\":\"" );
+ if ( msgPos != std::string::npos )
+ {
+ msgPos += 11;
+ size_t endPos = line.find( "\"", msgPos );
+ if ( endPos != std::string::npos )
+ {
+ std::string msg = line.substr( msgPos, endPos - msgPos );
+ UTIL_ClientPrintAll( HUD_PRINTTALK, "[OV JS] %s", msg.c_str() );
+ }
+ }
+ }
+ else if ( line.find( "\"type\":\"entity_spawn\"" ) != std::string::npos )
+ {
+ std::string type = "prop_physics_override";
+ std::string model = "";
+ Vector pos( 0, 0, 0 );
+
+ size_t tPos = line.find( "\"type\":\"" );
+ if ( tPos != std::string::npos )
+ {
+ size_t dataPos = line.find( "\"data\":{" );
+ if ( dataPos != std::string::npos )
+ {
+ size_t innerType = line.find( "\"type\":\"", dataPos );
+ if ( innerType != std::string::npos )
+ {
+ innerType += 8;
+ size_t end = line.find( "\"", innerType );
+ if ( end != std::string::npos )
+ {
+ type = line.substr( innerType, end - innerType );
+ }
+ }
+ size_t innerModel = line.find( "\"model\":\"", dataPos );
+ if ( innerModel != std::string::npos )
+ {
+ innerModel += 9;
+ size_t end = line.find( "\"", innerModel );
+ if ( end != std::string::npos )
+ {
+ model = line.substr( innerModel, end - innerModel );
+ }
+ }
+ size_t xPos = line.find( "\"x\":", dataPos );
+ size_t yPos = line.find( "\"y\":", dataPos );
+ size_t zPos = line.find( "\"z\":", dataPos );
+ if ( xPos != std::string::npos && yPos != std::string::npos && zPos != std::string::npos )
+ {
+ pos.x = atof( line.c_str() + xPos + 4 );
+ pos.y = atof( line.c_str() + yPos + 4 );
+ pos.z = atof( line.c_str() + zPos + 4 );
+ }
+ }
+ }
+
+ if ( !model.empty() )
+ {
+ CBaseEntity::PrecacheModel( model.c_str() );
+ CBaseEntity *pEnt = CreateEntityByName( type.c_str() );
+ if ( pEnt )
+ {
+ pEnt->KeyValue( "model", model.c_str() );
+ pEnt->SetAbsOrigin( pos );
+ DispatchSpawn( pEnt );
+ pEnt->Activate();
+ Msg( "[OV JS] Spawned entity %s with model %s at (%f, %f, %f)\n",
+ type.c_str(), model.c_str(), pos.x, pos.y, pos.z );
+ }
+ }
+ }
+ }
+};
+
+static COpenVibeJsBridge g_OpenVibeJsBridge;
+
+void OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer )
+{
+
+ CHL2MP_Player *pHL2MP = ToHL2MPPlayer( pPlayer );
+ if ( pHL2MP )
+ OpenVibeJS_Server_PlayerDisconnected( pHL2MP );
+}
+
+void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller )
+{
+
+ OpenVibeJS_Server_PlayerDeath( pPlayer, pKiller, NULL );
+}
+
+void OpenVibe_OnFrame()
+{
+
+ OpenVibeJS_ServerThink();
+}
+
+static void OV_JsCmd_f( const CCommand &args )
+{
+ if ( args.ArgC() < 2 )
+ {
+ Msg( "Usage: ov_js_cmd <command_or_json>\n" );
+ return;
+ }
+ g_OpenVibeJsBridge.OnCommand( args.ArgS() );
+}
+
+static ConCommand ov_js_cmd(
+ "ov_js_cmd",
+ OV_JsCmd_f,
+ "Send a command or JSON message to the OpenVibe.JS runtime.",
+ FCVAR_GAMEDLL );
new file mode 100644
new file mode 100644
@@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
+#include "cbase.h"
+#include "ov_js_bindings.h"
+#include "ov_js_runtime.h"
+#include "ov_js_player.h"
+#include "hl2mp_player.h"
+#include "util.h"
+
+#include "tier0/memdbgon.h"
+
+static COpenVibeJSRuntime *g_OVRuntime = nullptr;
+
+static JSValue OVJS_log(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_UNDEFINED;
+ const char *msg = JS_ToCString(ctx, argv[0]);
+ if (msg) { Msg("[OV JS] %s\n", msg); JS_FreeCString(ctx, msg); }
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_warn(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_UNDEFINED;
+ const char *msg = JS_ToCString(ctx, argv[0]);
+ if (msg) { Warning("[OV JS] %s\n", msg); JS_FreeCString(ctx, msg); }
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_error(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_UNDEFINED;
+ const char *msg = JS_ToCString(ctx, argv[0]);
+ if (msg) { Warning("[OV JS ERROR] %s\n", msg); JS_FreeCString(ctx, msg); }
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_getMode(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ return JS_NewString(ctx, g_OVRuntime ? g_OVRuntime->GetMode() : "unknown");
+}
+
+static JSValue OVJS_getMapName(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ return JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");
+}
+
+static JSValue OVJS_time(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ return JS_NewFloat64(ctx, gpGlobals ? gpGlobals->curtime : 0.0);
+}
+
+static JSValue OVJS_players(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ JSValue arr = JS_NewArray(ctx);
+ uint32 index = 0;
+
+ for (int i = 1; i <= gpGlobals->maxClients; ++i)
+ {
+ CBasePlayer *base = UTIL_PlayerByIndex(i);
+ if (!base) continue;
+
+ CHL2MP_Player *player = ToHL2MPPlayer(base);
+ if (!player) continue;
+
+ JS_SetPropertyUint32(ctx, arr, index++, OVJS_NewPlayer(ctx, player));
+ }
+
+ return arr;
+}
+
+static JSValue OVJS_playerByUserId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_NULL;
+
+ int32 userId = 0;
+ JS_ToInt32(ctx, &userId, argv[0]);
+
+ CHL2MP_Player *player = OVJS_ResolvePlayerByUserId(userId);
+ return player ? OVJS_NewPlayer(ctx, player) : JS_NULL;
+}
+
+static JSValue OVJS_broadcast(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_UNDEFINED;
+
+ const char *msg = JS_ToCString(ctx, argv[0]);
+ if (!msg) return JS_UNDEFINED;
+
+ UTIL_ClientPrintAll(HUD_PRINTTALK, msg);
+
+ JS_FreeCString(ctx, msg);
+ return JS_UNDEFINED;
+}
+
+static bool OVJS_IsAllowedServerCommand(const char *cmd)
+{
+ return cmd && (!Q_strnicmp(cmd, "ov_", 3) || !Q_strnicmp(cmd, "say ", 4));
+}
+
+static JSValue OVJS_serverCommand(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_UNDEFINED;
+
+ const char *cmd = JS_ToCString(ctx, argv[0]);
+ if (!cmd) return JS_UNDEFINED;
+
+ if (OVJS_IsAllowedServerCommand(cmd))
+ engine->ServerCommand(UTIL_VarArgs("%s\n", cmd));
+ else
+ Warning("[OV JS] blocked serverCommand: %s\n", cmd);
+
+ JS_FreeCString(ctx, cmd);
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_fireHook(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (!g_OVRuntime || argc < 1) return JS_UNDEFINED;
+
+ const char *hookName = JS_ToCString(ctx, argv[0]);
+ if (!hookName) return JS_UNDEFINED;
+
+ JSValue result = g_OVRuntime->CallHookRaw(hookName, argc - 1, argv + 1);
+
+ JS_FreeCString(ctx, hookName);
+ return result;
+}
+
+static JSValue OVJS_reward(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ Warning("[OV JS] OV.reward stubbed; backend reward wiring comes next.\n");
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_endMatch(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ Warning("[OV JS] OV.endMatch stubbed; backend match wiring comes next.\n");
+ return JS_UNDEFINED;
+}
+
+static const JSCFunctionListEntry OVFuncs[] =
+{
+ JS_CFUNC_DEF("log", 1, OVJS_log),
+ JS_CFUNC_DEF("warn", 1, OVJS_warn),
+ JS_CFUNC_DEF("error", 1, OVJS_error),
+ JS_CFUNC_DEF("getMode", 0, OVJS_getMode),
+ JS_CFUNC_DEF("getMapName", 0, OVJS_getMapName),
+ JS_CFUNC_DEF("time", 0, OVJS_time),
+ JS_CFUNC_DEF("players", 0, OVJS_players),
+ JS_CFUNC_DEF("playerByUserId", 1, OVJS_playerByUserId),
+ JS_CFUNC_DEF("broadcast", 1, OVJS_broadcast),
+ JS_CFUNC_DEF("serverCommand", 1, OVJS_serverCommand),
+ JS_CFUNC_DEF("fireHook", 1, OVJS_fireHook),
+ JS_CFUNC_DEF("reward", 4, OVJS_reward),
+ JS_CFUNC_DEF("endMatch", 1, OVJS_endMatch),
+};
+
+void OVJS_RegisterNativeBindings(JSContext *ctx, COpenVibeJSRuntime *runtime)
+{
+ g_OVRuntime = runtime;
+
+ OVJS_RegisterPlayerClass(ctx);
+
+ JSValue global = JS_GetGlobalObject(ctx);
+ JSValue ov = JS_NewObject(ctx);
+
+ JS_SetPropertyFunctionList(ctx, ov, OVFuncs, ARRAYSIZE(OVFuncs));
+ JS_SetPropertyStr(ctx, global, "OV", ov);
+
+ JS_FreeValue(ctx, global);
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+#pragma once
+
+#include "openvibe/third_party/quickjs/quickjs.h"
+
+class COpenVibeJSRuntime;
+
+void OVJS_RegisterNativeBindings(JSContext *ctx, COpenVibeJSRuntime *runtime);
new file mode 100644
new file mode 100644
@@ -0,0 +1,210 @@
@@ -0,0 +1,210 @@
+#include "cbase.h"
+#include "hl2mp_player.h"
+#include "player.h"
+#include "util.h"
+#include "ov_js_player.h"
+
+#include "tier0/memdbgon.h"
+
+struct OVJSPlayerHandle
+{
+ int userId;
+ int entIndex;
+};
+
+static JSClassID g_OVJSPlayerClassID;
+
+CHL2MP_Player *OVJS_ResolvePlayerByUserId(int userId)
+{
+ for (int i = 1; i <= gpGlobals->maxClients; ++i)
+ {
+ CBasePlayer *base = UTIL_PlayerByIndex(i);
+ if (!base)
+ continue;
+
+ CHL2MP_Player *player = ToHL2MPPlayer(base);
+ if (!player)
+ continue;
+
+ if (player->GetUserID() == userId)
+ return player;
+ }
+
+ return nullptr;
+}
+
+static void OVJS_PlayerFinalizer(JSRuntime *rt, JSValue val)
+{
+ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque(val, g_OVJSPlayerClassID));
+ delete handle;
+}
+
+CHL2MP_Player *OVJS_GetPlayerFromThis(JSContext *ctx, JSValueConst thisVal)
+{
+ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));
+ if (!handle)
+ return nullptr;
+
+ CHL2MP_Player *player = OVJS_ResolvePlayerByUserId(handle->userId);
+ if (!player)
+ return nullptr;
+
+ if (player->entindex() != handle->entIndex)
+ return nullptr;
+
+ return player;
+}
+
+static JSValue OVJS_Player_userId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));
+ return handle ? JS_NewInt32(ctx, handle->userId) : JS_UNDEFINED;
+}
+
+static JSValue OVJS_Player_entIndex(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));
+ return handle ? JS_NewInt32(ctx, handle->entIndex) : JS_UNDEFINED;
+}
+
+static JSValue OVJS_Player_name(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ return JS_NewString(ctx, player ? player->GetPlayerName() : "");
+}
+
+static JSValue OVJS_Player_steamId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ if (!player)
+ return JS_NewString(ctx, "");
+
+ const char *netid = engine->GetPlayerNetworkIDString(player->edict());
+ return JS_NewString(ctx, netid ? netid : "");
+}
+
+static JSValue OVJS_Player_health(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ return JS_NewInt32(ctx, player ? player->GetHealth() : 0);
+}
+
+static JSValue OVJS_Player_setHealth(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ if (!player || argc < 1)
+ return JS_UNDEFINED;
+
+ int32 value = 0;
+ JS_ToInt32(ctx, &value, argv[0]);
+
+ if (value < 0) value = 0;
+ if (value > 1000) value = 1000;
+
+ player->SetHealth(value);
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_Player_team(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ return JS_NewInt32(ctx, player ? player->GetTeamNumber() : 0);
+}
+
+static JSValue OVJS_Player_setTeam(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ if (!player || argc < 1)
+ return JS_UNDEFINED;
+
+ int32 team = 0;
+ JS_ToInt32(ctx, &team, argv[0]);
+
+ if (team >= 0 && team <= 4)
+ player->ChangeTeam(team);
+
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_Player_chat(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ if (!player || argc < 1)
+ return JS_UNDEFINED;
+
+ const char *msg = JS_ToCString(ctx, argv[0]);
+ if (!msg)
+ return JS_UNDEFINED;
+
+ ClientPrint(player, HUD_PRINTTALK, msg);
+
+ JS_FreeCString(ctx, msg);
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_Player_runCommand(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ if (!player || argc < 1)
+ return JS_UNDEFINED;
+
+ const char *cmd = JS_ToCString(ctx, argv[0]);
+ if (!cmd)
+ return JS_UNDEFINED;
+
+ if (!Q_strnicmp(cmd, "ov_", 3))
+ engine->ClientCommand(player->edict(), "%s\n", cmd);
+ else
+ Warning("[OV JS] blocked player command: %s\n", cmd);
+
+ JS_FreeCString(ctx, cmd);
+ return JS_UNDEFINED;
+}
+
+static const JSCFunctionListEntry OVJSPlayerProtoFuncs[] =
+{
+ JS_CFUNC_DEF("userId", 0, OVJS_Player_userId),
+ JS_CFUNC_DEF("entIndex", 0, OVJS_Player_entIndex),
+ JS_CFUNC_DEF("steamId", 0, OVJS_Player_steamId),
+ JS_CFUNC_DEF("name", 0, OVJS_Player_name),
+ JS_CFUNC_DEF("health", 0, OVJS_Player_health),
+ JS_CFUNC_DEF("setHealth", 1, OVJS_Player_setHealth),
+ JS_CFUNC_DEF("team", 0, OVJS_Player_team),
+ JS_CFUNC_DEF("setTeam", 1, OVJS_Player_setTeam),
+ JS_CFUNC_DEF("chat", 1, OVJS_Player_chat),
+ JS_CFUNC_DEF("runCommand", 1, OVJS_Player_runCommand),
+};
+
+void OVJS_RegisterPlayerClass(JSContext *ctx)
+{
+ JSRuntime *rt = JS_GetRuntime(ctx);
+
+ JS_NewClassID(&g_OVJSPlayerClassID);
+
+ JSClassDef cls = {};
+ cls.class_name = "OpenVibePlayer";
+ cls.finalizer = OVJS_PlayerFinalizer;
+
+ JS_NewClass(rt, g_OVJSPlayerClassID, &cls);
+
+ JSValue proto = JS_NewObject(ctx);
+ JS_SetPropertyFunctionList(ctx, proto, OVJSPlayerProtoFuncs, ARRAYSIZE(OVJSPlayerProtoFuncs));
+ JS_SetClassProto(ctx, g_OVJSPlayerClassID, proto);
+}
+
+JSValue OVJS_NewPlayer(JSContext *ctx, CHL2MP_Player *player)
+{
+ if (!player)
+ return JS_NULL;
+
+ JSValue obj = JS_NewObjectClass(ctx, g_OVJSPlayerClassID);
+ if (JS_IsException(obj))
+ return obj;
+
+ OVJSPlayerHandle *handle = new OVJSPlayerHandle();
+ handle->userId = player->GetUserID();
+ handle->entIndex = player->entindex();
+
+ JS_SetOpaque(obj, handle);
+ return obj;
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+#pragma once
+
+#include "openvibe/third_party/quickjs/quickjs.h"
+
+class CHL2MP_Player;
+
+void OVJS_RegisterPlayerClass(JSContext *ctx);
+JSValue OVJS_NewPlayer(JSContext *ctx, CHL2MP_Player *player);
+CHL2MP_Player *OVJS_ResolvePlayerByUserId(int userId);
+CHL2MP_Player *OVJS_GetPlayerFromThis(JSContext *ctx, JSValueConst thisVal);
@@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
#include "cbase.h"
#include "cbase.h"
-#include "ov_js_runtime.h"
#include "filesystem.h"
#include "filesystem.h"
+#include "ov_js_runtime.h"
+#include "ov_js_bindings.h"
#include "tier0/memdbgon.h"
#include "tier0/memdbgon.h"
@@ -16,13 +17,12 @@ COpenVibeJSRuntime::~COpenVibeJSRuntime()
@@ -16,13 +17,12 @@ COpenVibeJSRuntime::~COpenVibeJSRuntime()
Shutdown();
Shutdown();
}
}
-bool COpenVibeJSRuntime::Init(bool bServerRealm, const char *pszMode, const char *pszRootDir)
+bool COpenVibeJSRuntime::Init(bool bServerRealm, const char *pszMode)
{
{
Shutdown();
Shutdown();
m_bServerRealm = bServerRealm;
m_bServerRealm = bServerRealm;
- Q_strncpy(m_szMode, pszMode ? pszMode : "hub", sizeof(m_szMode));
- Q_strncpy(m_szRootDir, pszRootDir ? pszRootDir : "js", sizeof(m_szRootDir));
+ Q_strncpy(m_szMode, pszMode && pszMode[0] ? pszMode : "hub", sizeof(m_szMode));
m_pRuntime = JS_NewRuntime();
m_pRuntime = JS_NewRuntime();
if (!m_pRuntime)
if (!m_pRuntime)
@@ -39,9 +39,15 @@ bool COpenVibeJSRuntime::Init(bool bServerRealm, const char *pszMode, const char
@@ -39,9 +39,15 @@ bool COpenVibeJSRuntime::Init(bool bServerRealm, const char *pszMode, const char
return false;
return false;
}
}
- // TODO: OV_RegisterNativeBindings(m_pCtx, this);
+ OVJS_RegisterNativeBindings(m_pCtx, this);
+
+ if (!LoadCoreFiles())
+ return false;
+
+ if (!LoadGamemode())
+ return false;
- return LoadCoreFiles() && LoadGamemode();
+ return true;
}
}
void COpenVibeJSRuntime::Shutdown()
void COpenVibeJSRuntime::Shutdown()
@@ -69,7 +75,7 @@ bool COpenVibeJSRuntime::Eval(const char *pszCode, const char *pszFilename)
@@ -69,7 +75,7 @@ bool COpenVibeJSRuntime::Eval(const char *pszCode, const char *pszFilename)
pszCode,
pszCode,
Q_strlen(pszCode),
Q_strlen(pszCode),
pszFilename ? pszFilename : "<openvibe>",
pszFilename ? pszFilename : "<openvibe>",
- JS_EVAL_TYPE_MODULE
+ JS_EVAL_TYPE_GLOBAL
);
);
if (JS_IsException(result))
if (JS_IsException(result))
@@ -85,9 +91,6 @@ bool COpenVibeJSRuntime::Eval(const char *pszCode, const char *pszFilename)
@@ -85,9 +91,6 @@ bool COpenVibeJSRuntime::Eval(const char *pszCode, const char *pszFilename)
bool COpenVibeJSRuntime::LoadFile(const char *pszPath)
bool COpenVibeJSRuntime::LoadFile(const char *pszPath)
{
{
- if (!pszPath || !pszPath[0])
- return false;
-
FileHandle_t file = filesystem->Open(pszPath, "rb", "MOD");
FileHandle_t file = filesystem->Open(pszPath, "rb", "MOD");
if (!file)
if (!file)
{
{
@@ -95,23 +98,23 @@ bool COpenVibeJSRuntime::LoadFile(const char *pszPath)
@@ -95,23 +98,23 @@ bool COpenVibeJSRuntime::LoadFile(const char *pszPath)
return false;
return false;
}
}
- const int size = filesystem->Size(file);
- if (size <= 0 || size > 512 * 1024)
+ int size = filesystem->Size(file);
+ if (size <= 0 || size > 1024 * 1024)
{
{
filesystem->Close(file);
filesystem->Close(file);
Warning("[OV JS] Refusing script %s size=%d\n", pszPath, size);
Warning("[OV JS] Refusing script %s size=%d\n", pszPath, size);
return false;
return false;
}
}
- CUtlVector<char> buffer;
- buffer.SetSize(size + 1);
-
- filesystem->Read(buffer.Base(), size, file);
+ char *buffer = new char[size + 1];
+ filesystem->Read(buffer, size, file);
filesystem->Close(file);
filesystem->Close(file);
-
buffer[size] = '\0';
buffer[size] = '\0';
- return Eval(buffer.Base(), pszPath);
+ bool ok = Eval(buffer, pszPath);
+ delete[] buffer;
+
+ return ok;
}
}
bool COpenVibeJSRuntime::LoadCoreFiles()
bool COpenVibeJSRuntime::LoadCoreFiles()
@@ -130,26 +133,15 @@ bool COpenVibeJSRuntime::LoadCoreFiles()
@@ -130,26 +133,15 @@ bool COpenVibeJSRuntime::LoadCoreFiles()
bool COpenVibeJSRuntime::LoadGamemode()
bool COpenVibeJSRuntime::LoadGamemode()
{
{
- char path[256];
-
- Q_snprintf(path, sizeof(path), "js/gamemodes/base/shared.js");
- LoadFile(path);
-
- Q_snprintf(path, sizeof(path), "js/gamemodes/%s/shared.js", m_szMode);
- LoadFile(path);
+ LoadFile("js/gamemodes/base/server.js");
- Q_snprintf(
- path,
- sizeof(path),
- "js/gamemodes/%s/%s.js",
- m_szMode,
- m_bServerRealm ? "server" : "client"
- );
+ char path[256];
+ Q_snprintf(path, sizeof(path), "js/gamemodes/%s/%s.js", m_szMode, m_bServerRealm ? "server" : "client");
return LoadFile(path);
return LoadFile(path);
}
}
-JSValue COpenVibeJSRuntime::CallHook(const char *pszHookName, int argc, JSValueConst *argv)
+JSValue COpenVibeJSRuntime::CallHookRaw(const char *pszHookName, int argc, JSValueConst *argv)
{
{
if (!m_pCtx)
if (!m_pCtx)
return JS_UNDEFINED;
return JS_UNDEFINED;
@@ -158,22 +150,27 @@ JSValue COpenVibeJSRuntime::CallHook(const char *pszHookName, int argc, JSValueC
@@ -158,22 +150,27 @@ JSValue COpenVibeJSRuntime::CallHook(const char *pszHookName, int argc, JSValueC
JSValue gamemode = JS_GetPropertyStr(m_pCtx, global, "gamemode");
JSValue gamemode = JS_GetPropertyStr(m_pCtx, global, "gamemode");
JSValue call = JS_GetPropertyStr(m_pCtx, gamemode, "call");
JSValue call = JS_GetPropertyStr(m_pCtx, gamemode, "call");
- JSValue hookName = JS_NewString(m_pCtx, pszHookName);
+ if (!JS_IsFunction(m_pCtx, call))
+ {
+ JS_FreeValue(m_pCtx, call);
+ JS_FreeValue(m_pCtx, gamemode);
+ JS_FreeValue(m_pCtx, global);
+ return JS_UNDEFINED;
+ }
+
+ JSValue *callArgs = new JSValue[argc + 1];
+ callArgs[0] = JS_NewString(m_pCtx, pszHookName);
- CUtlVector<JSValueConst> args;
- args.AddToTail(hookName);
for (int i = 0; i < argc; ++i)
for (int i = 0; i < argc; ++i)
- args.AddToTail(argv[i]);
+ callArgs[i + 1] = JS_DupValue(m_pCtx, argv[i]);
- JSValue result = JS_Call(
- m_pCtx,
- call,
- gamemode,
- args.Count(),
- args.Base()
- );
+ JSValue result = JS_Call(m_pCtx, call, gamemode, argc + 1, callArgs);
+
+ for (int i = 0; i < argc + 1; ++i)
+ JS_FreeValue(m_pCtx, callArgs[i]);
+
+ delete[] callArgs;
- JS_FreeValue(m_pCtx, hookName);
JS_FreeValue(m_pCtx, call);
JS_FreeValue(m_pCtx, call);
JS_FreeValue(m_pCtx, gamemode);
JS_FreeValue(m_pCtx, gamemode);
JS_FreeValue(m_pCtx, global);
JS_FreeValue(m_pCtx, global);
@@ -188,6 +185,35 @@ JSValue COpenVibeJSRuntime::CallHook(const char *pszHookName, int argc, JSValueC
@@ -188,6 +185,35 @@ JSValue COpenVibeJSRuntime::CallHook(const char *pszHookName, int argc, JSValueC
return result;
return result;
}
}
+void COpenVibeJSRuntime::CallHookVoid(const char *pszHookName, int argc, JSValueConst *argv)
+{
+ if (!m_pCtx)
+ return;
+
+ JSValue result = CallHookRaw(pszHookName, argc, argv);
+ JS_FreeValue(m_pCtx, result);
+}
+
+bool COpenVibeJSRuntime::CallHookBool(const char *pszHookName, bool *pOut, int argc, JSValueConst *argv)
+{
+ if (!m_pCtx)
+ return false;
+
+ JSValue result = CallHookRaw(pszHookName, argc, argv);
+
+ if (JS_IsUndefined(result))
+ {
+ JS_FreeValue(m_pCtx, result);
+ return false;
+ }
+
+ if (pOut)
+ *pOut = JS_ToBool(m_pCtx, result) != 0;
+
+ JS_FreeValue(m_pCtx, result);
+ return true;
+}
+
void COpenVibeJSRuntime::PrintException(const char *pszWhere)
void COpenVibeJSRuntime::PrintException(const char *pszWhere)
{
{
JSValue exception = JS_GetException(m_pCtx);
JSValue exception = JS_GetException(m_pCtx);
@@ -1,8 +1,6 @@
@@ -1,8 +1,6 @@
#pragma once
#pragma once
-#include "quickjs.h"
-
-class CBasePlayer;
+#include "openvibe/third_party/quickjs/quickjs.h"
class COpenVibeJSRuntime
class COpenVibeJSRuntime
{
{
@@ -10,15 +8,18 @@ public:
@@ -10,15 +8,18 @@ public:
COpenVibeJSRuntime();
COpenVibeJSRuntime();
~COpenVibeJSRuntime();
~COpenVibeJSRuntime();
- bool Init(bool bServerRealm, const char *pszMode, const char *pszRootDir);
+ bool Init(bool bServerRealm, const char *pszMode);
void Shutdown();
void Shutdown();
bool LoadFile(const char *pszPath);
bool LoadFile(const char *pszPath);
bool Eval(const char *pszCode, const char *pszFilename);
bool Eval(const char *pszCode, const char *pszFilename);
- JSValue CallHook(const char *pszHookName, int argc = 0, JSValueConst *argv = nullptr);
+ JSValue CallHookRaw(const char *pszHookName, int argc = 0, JSValueConst *argv = nullptr);
+ void CallHookVoid(const char *pszHookName, int argc = 0, JSValueConst *argv = nullptr);
+ bool CallHookBool(const char *pszHookName, bool *pOut, int argc = 0, JSValueConst *argv = nullptr);
JSContext *Context() { return m_pCtx; }
JSContext *Context() { return m_pCtx; }
+ const char *GetMode() const { return m_szMode; }
private:
private:
void PrintException(const char *pszWhere);
void PrintException(const char *pszWhere);
@@ -27,8 +28,6 @@ private:
@@ -27,8 +28,6 @@ private:
JSRuntime *m_pRuntime = nullptr;
JSRuntime *m_pRuntime = nullptr;
JSContext *m_pCtx = nullptr;
JSContext *m_pCtx = nullptr;
-
bool m_bServerRealm = false;
bool m_bServerRealm = false;
char m_szMode[64]{};
char m_szMode[64]{};
- char m_szRootDir[512]{};
};
};
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+# QuickJS vendored for OpenVibe: Source
+
+Vendored QuickJS core files for OpenVibe's embedded JavaScript runtime.
+
+Excluded intentionally:
+- qjs.c
+- qjsc.c
+- quickjs-libc.c
+- quickjs-libc.h
+
+Reason:
+OpenVibe embeds QuickJS as a sandboxed script VM. Community scripts should not
+receive raw std/os/filesystem/process APIs.
@@ -17,6 +17,14 @@ copy_file() {
@@ -17,6 +17,14 @@ copy_file() {
echo "[openvibe-sdk] copied ${dst#$SDK/}"
echo "[openvibe-sdk] copied ${dst#$SDK/}"
}
}
+copy_tree() {
+ local src="$1"
+ local dst="$2"
+ mkdir -p "$dst"
+ rsync -a --delete "$src/" "$dst/"
+ echo "[openvibe-sdk] copied tree ${dst#$SDK/}"
+}
+
copy_file "$ROOT/sdk/openvibe/client/hl2mp/openvibe_client.cpp" \
copy_file "$ROOT/sdk/openvibe/client/hl2mp/openvibe_client.cpp" \
"$SDK/src/game/client/hl2mp/openvibe_client.cpp"
"$SDK/src/game/client/hl2mp/openvibe_client.cpp"
copy_file "$ROOT/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp" \
copy_file "$ROOT/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp" \
@@ -24,20 +32,75 @@ copy_file "$ROOT/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp" \
@@ -24,20 +32,75 @@ copy_file "$ROOT/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp" \
copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_server.cpp" \
copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_server.cpp" \
"$SDK/src/game/server/hl2mp/openvibe_server.cpp"
"$SDK/src/game/server/hl2mp/openvibe_server.cpp"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_runtime.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_runtime.cpp"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_bindings.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_bindings.cpp"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_player.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_player.cpp"
+
+copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.h" \
+ "$SDK/src/game/server/hl2mp/openvibe_js_server.h"
+copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp" \
+ "$SDK/src/game/server/hl2mp/openvibe_js_server.cpp"
+
+copy_tree "$ROOT/sdk/openvibe/third_party/quickjs" \
+ "$SDK/src/game/shared/openvibe/third_party/quickjs"
+
+QJS_C="$SDK/src/game/shared/openvibe/third_party/quickjs/quickjs.c"
+if [[ -f "$QJS_C" ]] && ! grep -q 'OPENVIBE_QUICKJS_CONFIG' "$QJS_C"; then
+ tmp="$(mktemp)"
+ cat > "$tmp" <<'QCFG'
+#ifndef OPENVIBE_QUICKJS_CONFIG
+#define OPENVIBE_QUICKJS_CONFIG
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#ifndef CONFIG_VERSION
+#define CONFIG_VERSION "openvibe"
+#endif
+#endif
+
+QCFG
+ cat "$QJS_C" >> "$tmp"
+ mv "$tmp" "$QJS_C"
+fi
+
CLIENT_VPC="$SDK/src/game/client/client_hl2mp.vpc"
CLIENT_VPC="$SDK/src/game/client/client_hl2mp.vpc"
SERVER_VPC="$SDK/src/game/server/server_hl2mp.vpc"
SERVER_VPC="$SDK/src/game/server/server_hl2mp.vpc"
HL2MP_CLIENT="$SDK/src/game/server/hl2mp/hl2mp_client.cpp"
HL2MP_CLIENT="$SDK/src/game/server/hl2mp/hl2mp_client.cpp"
HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp"
HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp"
+GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"
+HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.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"
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"
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"
+perl -0pi -e '
+ s/^.*hl2mp\\openvibe_server\.cpp.*\n//mg;
+ s/^.*hl2mp\\openvibe_js_server\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_bindings\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_player\.cpp.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\quickjs\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libregexp\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libunicode\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\cutils\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\dtoa\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libbf\.c.*\n//mg;
+ s/(\$File\s+"hl2mp\\hl2mp_player\.h"\n)/$1\t\t\t\$File\t"hl2mp\\openvibe_server.cpp"\n\t\t\t\$File\t"hl2mp\\openvibe_js_server.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_runtime.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_bindings.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_player.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\quickjs.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libregexp.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libunicode.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\cutils.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\dtoa.c"\n/s;
+' "$SERVER_VPC"
echo "[openvibe-sdk] patched server_hl2mp.vpc"
echo "[openvibe-sdk] patched server_hl2mp.vpc"
if ! grep -q 'OpenVibe_OnClientActive' "$HL2MP_CLIENT"; then
if ! grep -q 'OpenVibe_OnClientActive' "$HL2MP_CLIENT"; then
sed -i '/void Host_Say/a void OpenVibe_OnClientActive( CHL2MP_Player *pPlayer );' "$HL2MP_CLIENT"
sed -i '/void Host_Say/a void OpenVibe_OnClientActive( CHL2MP_Player *pPlayer );' "$HL2MP_CLIENT"
- sed -i '/FinishClientPutInServer( pPlayer );/a \tOpenVibe_OnClientActive( pPlayer );' "$HL2MP_CLIENT"
+ sed -i '/FinishClientPutInServer( pPlayer );/a \ OpenVibe_OnClientActive( pPlayer );' "$HL2MP_CLIENT"
echo "[openvibe-sdk] patched hl2mp_client.cpp arrival hook"
echo "[openvibe-sdk] patched hl2mp_client.cpp arrival hook"
fi
fi
@@ -51,9 +114,6 @@ fi
@@ -51,9 +114,6 @@ fi
perl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"
perl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"
-GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"
-HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.cpp"
-
if [[ -f "$GAMEINTERFACE" ]]; then
if [[ -f "$GAMEINTERFACE" ]]; then
if ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; then
if ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; then
sed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"
sed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"
@@ -74,6 +134,40 @@ if [[ -f "$HL2MP_PLAYER" ]]; then
@@ -74,6 +134,40 @@ if [[ -f "$HL2MP_PLAYER" ]]; then
sed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"
sed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"
echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"
echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"
fi
fi
+
+ if ! grep -q 'OpenVibeJS_Server_PlayerSpawn' "$HL2MP_PLAYER"; then
+ sed -i '/CHL2MP_Player::Spawn/i void OpenVibeJS_Server_PlayerSpawn( CHL2MP_Player *pPlayer );' "$HL2MP_PLAYER"
+ sed -i '0,/BaseClass::Spawn();/s//BaseClass::Spawn();\n\tOpenVibeJS_Server_PlayerSpawn( this );/' "$HL2MP_PLAYER"
+ echo "[openvibe-sdk] patched hl2mp_player.cpp spawn hook"
+ fi
+fi
+
+if [[ -f "$HL2MP_CLIENT" ]] && ! grep -q 'OpenVibeJS_Server_PlayerSay' "$HL2MP_CLIENT"; then
+ sed -i '/void Host_Say/a bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *pPlayer, const char *pszText );' "$HL2MP_CLIENT"
+ python3 - "$HL2MP_CLIENT" <<'PY'
+from pathlib import Path
+import re, sys
+
+p = Path(sys.argv[1])
+s = p.read_text()
+
+# Best-effort patch for common Source SDK 2013 HL2MP Host_Say shape:
+# CHL2MP_Player *client = ...
+# const char *p = args.ArgS();
+if "OpenVibeJS_Server_PlayerSay( client," not in s:
+ s2 = re.sub(
+ r'(const char \*p\s*=\s*args\.ArgS\(\);\s*)',
+ r'\1\n\tif ( client && OpenVibeJS_Server_PlayerSay( client, p ) )\n\t\treturn;\n',
+ s,
+ count=1,
+ )
+ if s2 != s:
+ s = s2
+ print("[openvibe-sdk] patched hl2mp_client.cpp PlayerSay hook")
+ else:
+ print("[openvibe-sdk] WARNING: could not auto-patch Host_Say PlayerSay body")
+p.write_text(s)
+PY
fi
fi
echo "[openvibe-sdk] Source SDK OpenVibe patch applied"
echo "[openvibe-sdk] Source SDK OpenVibe patch applied"
new file mode 100755
new file mode 100755
@@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
+#!/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"
+HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.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"
+
+if ! grep -q 'OpenVibe_OnClientModeInit' "$HL2MP_CLIENTMODE"; then
+ sed -i '/#include "ienginevgui.h"/a void OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"
+ sed -i '/BaseClass::Init();/a \ OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"
+ echo "[openvibe-sdk] patched clientmode_hl2mpnormal.cpp OpenVibe menu hook"
+fi
+
+perl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"
+
+GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"
+HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.cpp"
+
+if [[ -f "$GAMEINTERFACE" ]]; then
+ if ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; then
+ sed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"
+ sed -i '/VPROF( "CServerGameDLL::GameFrame" );/a #ifdef HL2MP\n\tOpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"
+ echo "[openvibe-sdk] patched gameinterface.cpp frame hook"
+ fi
+
+ if ! grep -q 'OpenVibe_OnClientDisconnect' "$GAMEINTERFACE"; then
+ perl -pi -e 's/void CServerGameClients::ClientDisconnect/#ifdef HL2MP\nvoid OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer );\n#endif\nvoid CServerGameClients::ClientDisconnect/' "$GAMEINTERFACE"
+ perl -0777 -pi -e 's/(void CServerGameClients::ClientDisconnect\( edict_t \*pEdict \)\s*\{\s*extern bool\s+g_fGameOver;\s*CBasePlayer \*player = \( CBasePlayer \* \)CBaseEntity::Instance\( pEdict \);)/$1\n#ifdef HL2MP\n\tif ( player ) { OpenVibe_OnClientDisconnect( player ); }\n#endif/g' "$GAMEINTERFACE"
+ echo "[openvibe-sdk] patched gameinterface.cpp disconnect hook"
+ fi
+fi
+
+if [[ -f "$HL2MP_PLAYER" ]]; then
+ if ! grep -q 'OpenVibe_OnPlayerDeath' "$HL2MP_PLAYER"; then
+ sed -i '/CHL2MP_Player::Event_Killed/i void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller );' "$HL2MP_PLAYER"
+ sed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"
+ echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"
+ fi
+fi
+
+echo "[openvibe-sdk] Source SDK OpenVibe patch applied"
new file mode 100755
new file mode 100755
@@ -0,0 +1,173 @@
@@ -0,0 +1,173 @@
+#!/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_tree() {
+ local src="$1"
+ local dst="$2"
+ mkdir -p "$dst"
+ rsync -a --delete "$src/" "$dst/"
+ echo "[openvibe-sdk] copied tree ${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"
+
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_runtime.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_runtime.cpp"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_bindings.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_bindings.cpp"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_player.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_player.cpp"
+
+copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.h" \
+ "$SDK/src/game/server/hl2mp/openvibe_js_server.h"
+copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp" \
+ "$SDK/src/game/server/hl2mp/openvibe_js_server.cpp"
+
+copy_tree "$ROOT/sdk/openvibe/third_party/quickjs" \
+ "$SDK/src/game/shared/openvibe/third_party/quickjs"
+
+QJS_C="$SDK/src/game/shared/openvibe/third_party/quickjs/quickjs.c"
+if [[ -f "$QJS_C" ]] && ! grep -q 'OPENVIBE_QUICKJS_CONFIG' "$QJS_C"; then
+ tmp="$(mktemp)"
+ cat > "$tmp" <<'QCFG'
+#ifndef OPENVIBE_QUICKJS_CONFIG
+#define OPENVIBE_QUICKJS_CONFIG
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#ifndef CONFIG_VERSION
+#define CONFIG_VERSION "openvibe"
+#endif
+#endif
+
+QCFG
+ cat "$QJS_C" >> "$tmp"
+ mv "$tmp" "$QJS_C"
+fi
+
+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"
+HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp"
+GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"
+HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.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/^.*hl2mp\\openvibe_js_server\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_bindings\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_player\.cpp.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\quickjs\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libregexp\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libunicode\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\cutils\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\dtoa\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libbf\.c.*\n//mg;
+ s/(\$File\s+"hl2mp\\hl2mp_player\.h"\n)/$1\t\t\t\$File\t"hl2mp\\openvibe_server.cpp"\n\t\t\t\$File\t"hl2mp\\openvibe_js_server.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_runtime.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_bindings.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_player.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\quickjs.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libregexp.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libunicode.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\cutils.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\dtoa.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libbf.c"\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 \ OpenVibe_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"
+
+if ! grep -q 'OpenVibe_OnClientModeInit' "$HL2MP_CLIENTMODE"; then
+ sed -i '/#include "ienginevgui.h"/a void OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"
+ sed -i '/BaseClass::Init();/a \ OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"
+ echo "[openvibe-sdk] patched clientmode_hl2mpnormal.cpp OpenVibe menu hook"
+fi
+
+perl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"
+
+if [[ -f "$GAMEINTERFACE" ]]; then
+ if ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; then
+ sed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"
+ sed -i '/VPROF( "CServerGameDLL::GameFrame" );/a #ifdef HL2MP\n\tOpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"
+ echo "[openvibe-sdk] patched gameinterface.cpp frame hook"
+ fi
+
+ if ! grep -q 'OpenVibe_OnClientDisconnect' "$GAMEINTERFACE"; then
+ perl -pi -e 's/void CServerGameClients::ClientDisconnect/#ifdef HL2MP\nvoid OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer );\n#endif\nvoid CServerGameClients::ClientDisconnect/' "$GAMEINTERFACE"
+ perl -0777 -pi -e 's/(void CServerGameClients::ClientDisconnect\( edict_t \*pEdict \)\s*\{\s*extern bool\s+g_fGameOver;\s*CBasePlayer \*player = \( CBasePlayer \* \)CBaseEntity::Instance\( pEdict \);)/$1\n#ifdef HL2MP\n\tif ( player ) { OpenVibe_OnClientDisconnect( player ); }\n#endif/g' "$GAMEINTERFACE"
+ echo "[openvibe-sdk] patched gameinterface.cpp disconnect hook"
+ fi
+fi
+
+if [[ -f "$HL2MP_PLAYER" ]]; then
+ if ! grep -q 'OpenVibe_OnPlayerDeath' "$HL2MP_PLAYER"; then
+ sed -i '/CHL2MP_Player::Event_Killed/i void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller );' "$HL2MP_PLAYER"
+ sed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"
+ echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"
+ fi
+
+ if ! grep -q 'OpenVibeJS_Server_PlayerSpawn' "$HL2MP_PLAYER"; then
+ sed -i '/CHL2MP_Player::Spawn/i void OpenVibeJS_Server_PlayerSpawn( CHL2MP_Player *pPlayer );' "$HL2MP_PLAYER"
+ sed -i '0,/BaseClass::Spawn();/s//BaseClass::Spawn();\n\tOpenVibeJS_Server_PlayerSpawn( this );/' "$HL2MP_PLAYER"
+ echo "[openvibe-sdk] patched hl2mp_player.cpp spawn hook"
+ fi
+fi
+
+if [[ -f "$HL2MP_CLIENT" ]] && ! grep -q 'OpenVibeJS_Server_PlayerSay' "$HL2MP_CLIENT"; then
+ sed -i '/void Host_Say/a bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *pPlayer, const char *pszText );' "$HL2MP_CLIENT"
+ python3 - "$HL2MP_CLIENT" <<'PY'
+from pathlib import Path
+import re, sys
+
+p = Path(sys.argv[1])
+s = p.read_text()
+
+# Best-effort patch for common Source SDK 2013 HL2MP Host_Say shape:
+# CHL2MP_Player *client = ...
+# const char *p = args.ArgS();
+if "OpenVibeJS_Server_PlayerSay( client," not in s:
+ s2 = re.sub(
+ r'(const char \*p\s*=\s*args\.ArgS\(\);\s*)',
+ r'\1\n\tif ( client && OpenVibeJS_Server_PlayerSay( client, p ) )\n\t\treturn;\n',
+ s,
+ count=1,
+ )
+ if s2 != s:
+ s = s2
+ print("[openvibe-sdk] patched hl2mp_client.cpp PlayerSay hook")
+ else:
+ print("[openvibe-sdk] WARNING: could not auto-patch Host_Say PlayerSay body")
+p.write_text(s)
+PY
+fi
+
+echo "[openvibe-sdk] Source SDK OpenVibe patch applied"
new file mode 100755
new file mode 100755
@@ -0,0 +1,173 @@
@@ -0,0 +1,173 @@
+#!/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_tree() {
+ local src="$1"
+ local dst="$2"
+ mkdir -p "$dst"
+ rsync -a --delete "$src/" "$dst/"
+ echo "[openvibe-sdk] copied tree ${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"
+
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_runtime.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_runtime.cpp"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_bindings.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_bindings.cpp"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_player.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_player.cpp"
+
+copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.h" \
+ "$SDK/src/game/server/hl2mp/openvibe_js_server.h"
+copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp" \
+ "$SDK/src/game/server/hl2mp/openvibe_js_server.cpp"
+
+copy_tree "$ROOT/sdk/openvibe/third_party/quickjs" \
+ "$SDK/src/game/shared/openvibe/third_party/quickjs"
+
+QJS_C="$SDK/src/game/shared/openvibe/third_party/quickjs/quickjs.c"
+if [[ -f "$QJS_C" ]] && ! grep -q 'OPENVIBE_QUICKJS_CONFIG' "$QJS_C"; then
+ tmp="$(mktemp)"
+ cat > "$tmp" <<'QCFG'
+#ifndef OPENVIBE_QUICKJS_CONFIG
+#define OPENVIBE_QUICKJS_CONFIG
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#ifndef CONFIG_VERSION
+#define CONFIG_VERSION "openvibe"
+#endif
+#endif
+
+QCFG
+ cat "$QJS_C" >> "$tmp"
+ mv "$tmp" "$QJS_C"
+fi
+
+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"
+HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp"
+GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"
+HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.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/^.*hl2mp\\openvibe_js_server\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_bindings\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_player\.cpp.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\quickjs\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libregexp\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libunicode\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\cutils\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\dtoa\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libbf\.c.*\n//mg;
+ s/(\$File\s+"hl2mp\\hl2mp_player\.h"\n)/$1\t\t\t\$File\t"hl2mp\\openvibe_server.cpp"\n\t\t\t\$File\t"hl2mp\\openvibe_js_server.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_runtime.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_bindings.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_player.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\quickjs.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libregexp.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libunicode.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\cutils.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\dtoa.c"\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 \ OpenVibe_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"
+
+if ! grep -q 'OpenVibe_OnClientModeInit' "$HL2MP_CLIENTMODE"; then
+ sed -i '/#include "ienginevgui.h"/a void OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"
+ sed -i '/BaseClass::Init();/a \ OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"
+ echo "[openvibe-sdk] patched clientmode_hl2mpnormal.cpp OpenVibe menu hook"
+fi
+
+perl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"
+
+if [[ -f "$GAMEINTERFACE" ]]; then
+ if ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; then
+ sed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"
+ sed -i '/VPROF( "CServerGameDLL::GameFrame" );/a #ifdef HL2MP\n\tOpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"
+ echo "[openvibe-sdk] patched gameinterface.cpp frame hook"
+ fi
+
+ if ! grep -q 'OpenVibe_OnClientDisconnect' "$GAMEINTERFACE"; then
+ perl -pi -e 's/void CServerGameClients::ClientDisconnect/#ifdef HL2MP\nvoid OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer );\n#endif\nvoid CServerGameClients::ClientDisconnect/' "$GAMEINTERFACE"
+ perl -0777 -pi -e 's/(void CServerGameClients::ClientDisconnect\( edict_t \*pEdict \)\s*\{\s*extern bool\s+g_fGameOver;\s*CBasePlayer \*player = \( CBasePlayer \* \)CBaseEntity::Instance\( pEdict \);)/$1\n#ifdef HL2MP\n\tif ( player ) { OpenVibe_OnClientDisconnect( player ); }\n#endif/g' "$GAMEINTERFACE"
+ echo "[openvibe-sdk] patched gameinterface.cpp disconnect hook"
+ fi
+fi
+
+if [[ -f "$HL2MP_PLAYER" ]]; then
+ if ! grep -q 'OpenVibe_OnPlayerDeath' "$HL2MP_PLAYER"; then
+ sed -i '/CHL2MP_Player::Event_Killed/i void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller );' "$HL2MP_PLAYER"
+ sed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"
+ echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"
+ fi
+
+ if ! grep -q 'OpenVibeJS_Server_PlayerSpawn' "$HL2MP_PLAYER"; then
+ sed -i '/CHL2MP_Player::Spawn/i void OpenVibeJS_Server_PlayerSpawn( CHL2MP_Player *pPlayer );' "$HL2MP_PLAYER"
+ sed -i '0,/BaseClass::Spawn();/s//BaseClass::Spawn();\n\tOpenVibeJS_Server_PlayerSpawn( this );/' "$HL2MP_PLAYER"
+ echo "[openvibe-sdk] patched hl2mp_player.cpp spawn hook"
+ fi
+fi
+
+if [[ -f "$HL2MP_CLIENT" ]] && ! grep -q 'OpenVibeJS_Server_PlayerSay' "$HL2MP_CLIENT"; then
+ sed -i '/void Host_Say/a bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *pPlayer, const char *pszText );' "$HL2MP_CLIENT"
+ python3 - "$HL2MP_CLIENT" <<'PY'
+from pathlib import Path
+import re, sys
+
+p = Path(sys.argv[1])
+s = p.read_text()
+
+# Best-effort patch for common Source SDK 2013 HL2MP Host_Say shape:
+# CHL2MP_Player *client = ...
+# const char *p = args.ArgS();
+if "OpenVibeJS_Server_PlayerSay( client," not in s:
+ s2 = re.sub(
+ r'(const char \*p\s*=\s*args\.ArgS\(\);\s*)',
+ r'\1\n\tif ( client && OpenVibeJS_Server_PlayerSay( client, p ) )\n\t\treturn;\n',
+ s,
+ count=1,
+ )
+ if s2 != s:
+ s = s2
+ print("[openvibe-sdk] patched hl2mp_client.cpp PlayerSay hook")
+ else:
+ print("[openvibe-sdk] WARNING: could not auto-patch Host_Say PlayerSay body")
+p.write_text(s)
+PY
+fi
+
+echo "[openvibe-sdk] Source SDK OpenVibe patch applied"
new file mode 100755
new file mode 100755
@@ -0,0 +1,104 @@
@@ -0,0 +1,104 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+QJS="$ROOT/sdk/openvibe/third_party/quickjs"
+
+cd "$ROOT"
+
+echo "[openvibe] forcing clean QuickJS re-vendor"
+rm -rf "$QJS"
+tools/vendor-quickjs.sh
+
+echo "[openvibe] replacing smoke-quickjs.sh with robust version"
+
+cat > tools/smoke-quickjs.sh <<'SMOKE'
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+QJS="$ROOT/sdk/openvibe/third_party/quickjs"
+BUILD="$ROOT/.tmp/quickjs-smoke"
+
+rm -rf "$BUILD"
+mkdir -p "$BUILD"
+
+cat > "$BUILD/smoke.c" <<'C'
+#include <stdio.h>
+#include "quickjs.h"
+
+int main(void) {
+ JSRuntime *rt = JS_NewRuntime();
+ JSContext *ctx = JS_NewContext(rt);
+
+ JSValue value = JS_Eval(ctx, "1 + 2 + 3", 9, "<smoke>", JS_EVAL_TYPE_GLOBAL);
+ int32_t out = 0;
+ JS_ToInt32(ctx, &out, value);
+
+ JS_FreeValue(ctx, value);
+ JS_FreeContext(ctx);
+ JS_FreeRuntime(rt);
+
+ printf("quickjs result=%d\n", out);
+ return out == 6 ? 0 : 1;
+}
+C
+
+sources=(
+ "$QJS/quickjs.c"
+ "$QJS/libregexp.c"
+ "$QJS/libunicode.c"
+ "$QJS/cutils.c"
+)
+
+[[ -f "$QJS/dtoa.c" ]] && sources+=("$QJS/dtoa.c")
+[[ -f "$QJS/libbf.c" ]] && sources+=("$QJS/libbf.c")
+
+cc \
+ -std=gnu11 \
+ -D_GNU_SOURCE \
+ -DCONFIG_VERSION=\"openvibe\" \
+ -I"$QJS" \
+ "$BUILD/smoke.c" \
+ "${sources[@]}" \
+ -lm \
+ -ldl \
+ -lpthread \
+ -o "$BUILD/smoke-quickjs"
+
+"$BUILD/smoke-quickjs"
+SMOKE
+
+chmod +x tools/smoke-quickjs.sh
+
+echo "[openvibe] patching apply-openvibe-sdk.sh to not require libbf.c blindly"
+
+python3 <<'PY'
+from pathlib import Path
+
+p = Path("tools/apply-openvibe-sdk.sh")
+s = p.read_text()
+
+# Remove hardcoded libbf VPC line insertion if present.
+s = s.replace(
+ '\\n\\t\\t\\t\\$File\\t"..\\\\shared\\\\openvibe\\\\third_party\\\\quickjs\\\\libbf.c"',
+ ''
+)
+
+# Also remove the cleanup regex for libbf so repeated runs don't matter.
+s = s.replace(
+ ' s/^.*openvibe\\\\\\\\third_party\\\\\\\\quickjs\\\\\\\\libbf\\\\.c.*\\n//mg;\\n',
+ ''
+)
+
+p.write_text(s)
+PY
+
+echo "[openvibe] quickjs folder now contains:"
+find "$QJS" -maxdepth 1 -type f -printf " - %f\n" | sort
+
+echo "[openvibe] running smoke test"
+tools/smoke-quickjs.sh
+
+echo
+echo "[openvibe] QuickJS vendor/smoke fixed."
new file mode 100755
new file mode 100755
@@ -0,0 +1,1665 @@
@@ -0,0 +1,1665 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+SDK="${OPENVIBE_SDK:-$ROOT/engine/source-sdk-2013}"
+STAMP="$(date +%Y%m%d-%H%M%S)"
+
+cd "$ROOT"
+
+need_file() {
+ [[ -f "$1" ]] || { echo "Missing required file: $1" >&2; exit 1; }
+}
+
+backup_file() {
+ local file="$1"
+ [[ -f "$file" ]] && cp "$file" "$file.bak.$STAMP"
+}
+
+mkdir -p \
+ tools \
+ sdk/openvibe/third_party/quickjs \
+ sdk/openvibe/shared \
+ sdk/openvibe/server/hl2mp \
+ game/openvibe.games/js/core \
+ game/openvibe.games/js/autorun/{server,client,shared} \
+ game/openvibe.games/js/gamemodes/{base,hub,prophunt,deathrun,fortwars,traitortown} \
+ game/openvibe.games/js/addons
+
+need_file "tools/apply-openvibe-sdk.sh"
+need_file "sdk/openvibe/server/hl2mp/openvibe_server.cpp"
+
+echo "[openvibe] writing QuickJS vendor script"
+
+cat > tools/vendor-quickjs.sh <<'VENDOR_QJS'
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+DEST="$ROOT/sdk/openvibe/third_party/quickjs"
+TMP="${TMPDIR:-/tmp}/openvibe-quickjs-vendor"
+
+rm -rf "$TMP"
+
+git clone --depth 1 https://github.com/bellard/quickjs.git "$TMP" || \
+git clone --depth 1 https://github.com/quickjs-ng/quickjs.git "$TMP"
+
+rm -rf "$DEST"
+mkdir -p "$DEST"
+
+rsync -av \
+ --include='quickjs.c' \
+ --include='quickjs.h' \
+ --include='quickjs-atom.h' \
+ --include='quickjs-opcode.h' \
+ --include='libregexp.c' \
+ --include='libregexp.h' \
+ --include='libregexp-opcode.h' \
+ --include='libunicode.c' \
+ --include='libunicode.h' \
+ --include='libunicode-table.h' \
+ --include='cutils.c' \
+ --include='cutils.h' \
+ --include='dtoa.c' \
+ --include='dtoa.h' \
+ --include='libbf.c' \
+ --include='libbf.h' \
+ --include='list.h' \
+ --include='LICENSE' \
+ --include='VERSION' \
+ --exclude='*' \
+ "$TMP/" \
+ "$DEST/"
+
+git -C "$TMP" rev-parse HEAD > "$DEST/UPSTREAM_COMMIT"
+
+cat > "$DEST/README.openvibe.md" <<'DOC'
+# QuickJS vendored for OpenVibe: Source
+
+Vendored QuickJS core files for OpenVibe's embedded JavaScript runtime.
+
+Excluded intentionally:
+- qjs.c
+- qjsc.c
+- quickjs-libc.c
+- quickjs-libc.h
+
+Reason:
+OpenVibe embeds QuickJS as a sandboxed script VM. Community scripts should not
+receive raw std/os/filesystem/process APIs.
+DOC
+
+echo "[openvibe] vendored QuickJS into $DEST"
+echo "[openvibe] upstream commit: $(cat "$DEST/UPSTREAM_COMMIT")"
+VENDOR_QJS
+
+chmod +x tools/vendor-quickjs.sh
+
+echo "[openvibe] writing QuickJS smoke test"
+
+cat > tools/smoke-quickjs.sh <<'SMOKE_QJS'
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+QJS="$ROOT/sdk/openvibe/third_party/quickjs"
+BUILD="$ROOT/.tmp/quickjs-smoke"
+
+rm -rf "$BUILD"
+mkdir -p "$BUILD"
+
+cat > "$BUILD/smoke.c" <<'C'
+#include <stdio.h>
+#include "quickjs.h"
+
+int main(void) {
+ JSRuntime *rt = JS_NewRuntime();
+ JSContext *ctx = JS_NewContext(rt);
+
+ JSValue value = JS_Eval(ctx, "1 + 2 + 3", 9, "<smoke>", JS_EVAL_TYPE_GLOBAL);
+ int32_t out = 0;
+ JS_ToInt32(ctx, &out, value);
+
+ JS_FreeValue(ctx, value);
+ JS_FreeContext(ctx);
+ JS_FreeRuntime(rt);
+
+ printf("quickjs result=%d\n", out);
+ return out == 6 ? 0 : 1;
+}
+C
+
+cc \
+ -std=c11 \
+ -D_GNU_SOURCE \
+ -DCONFIG_VERSION=\"openvibe\" \
+ -I"$QJS" \
+ "$BUILD/smoke.c" \
+ "$QJS/quickjs.c" \
+ "$QJS/libregexp.c" \
+ "$QJS/libunicode.c" \
+ "$QJS/cutils.c" \
+ "$QJS/dtoa.c" \
+ "$QJS/libbf.c" \
+ -lm \
+ -ldl \
+ -lpthread \
+ -o "$BUILD/smoke-quickjs"
+
+"$BUILD/smoke-quickjs"
+SMOKE_QJS
+
+chmod +x tools/smoke-quickjs.sh
+
+if [[ ! -f sdk/openvibe/third_party/quickjs/quickjs.c ]]; then
+ echo "[openvibe] vendoring QuickJS"
+ tools/vendor-quickjs.sh
+else
+ echo "[openvibe] QuickJS already vendored; skipping vendor step"
+fi
+
+echo "[openvibe] writing JS runtime files"
+
+cat > game/openvibe.games/js/core/hook.js <<'JS_HOOK'
+(function () {
+ const hooks = new Map();
+
+ function getEvent(name) {
+ let map = hooks.get(name);
+ if (!map) {
+ map = new Map();
+ hooks.set(name, map);
+ }
+ return map;
+ }
+
+ globalThis.hook = {
+ add(name, id, fn) {
+ if (typeof name !== "string" || !name) throw new Error("hook name must be string");
+ if (typeof id !== "string" || !id) throw new Error("hook id must be string");
+ if (typeof fn !== "function") throw new Error("hook callback must be function");
+ getEvent(name).set(id, fn);
+ },
+
+ remove(name, id) {
+ const map = hooks.get(name);
+ return map ? map.delete(id) : false;
+ },
+
+ run(name, ...args) {
+ const map = hooks.get(name);
+ if (!map) return undefined;
+
+ for (const [id, fn] of map.entries()) {
+ try {
+ const result = fn(...args);
+ if (result !== undefined) return result;
+ } catch (err) {
+ OV.error(`[hook:${name}:${id}] ${err && err.stack ? err.stack : err}`);
+ }
+ }
+
+ return undefined;
+ },
+
+ list() {
+ const out = {};
+ for (const [eventName, map] of hooks.entries()) out[eventName] = Array.from(map.keys());
+ return out;
+ },
+
+ clear() {
+ hooks.clear();
+ }
+ };
+})();
+JS_HOOK
+
+cat > game/openvibe.games/js/core/gamemode.js <<'JS_GM'
+(function () {
+ let current = null;
+
+ globalThis.gamemode = {
+ set(gm) {
+ if (!gm || typeof gm !== "object") throw new Error("gamemode.set requires object");
+ current = gm;
+ },
+
+ get() {
+ return current;
+ },
+
+ call(name, ...args) {
+ const hookResult = hook.run(name, ...args);
+ if (hookResult !== undefined) return hookResult;
+
+ if (current && typeof current[name] === "function") {
+ return current[name](...args);
+ }
+
+ return undefined;
+ }
+ };
+})();
+JS_GM
+
+cat > game/openvibe.games/js/bridge.js <<'JS_BRIDGE'
+OV.log("bridge.js loaded");
+
+globalThis.console = {
+ log: (...args) => OV.log(args.map(String).join(" ")),
+ warn: (...args) => OV.warn(args.map(String).join(" ")),
+ error: (...args) => OV.error(args.map(String).join(" "))
+};
+
+globalThis.game = {
+ mode: () => OV.getMode(),
+ map: () => OV.getMapName(),
+ time: () => OV.time(),
+ broadcast: (msg) => OV.broadcast(String(msg)),
+ serverCommand: (cmd) => OV.serverCommand(String(cmd))
+};
+
+globalThis.round = {
+ start: () => OV.fireHook("RoundStart"),
+ end: () => OV.fireHook("RoundEnd")
+};
+JS_BRIDGE
+
+cat > game/openvibe.games/js/gamemodes/base/server.js <<'JS_BASE'
+const GM = {
+ mode: "base",
+ name: "OpenVibe Base",
+
+ Initialize() {
+ OV.log("Base Initialize fired");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source.");
+ },
+
+ PlayerSpawn(_ply) {},
+
+ PlayerDeath(_victim, _attacker) {},
+
+ PlayerDisconnected(_ply) {},
+
+ PlayerSay(_ply, _text) {
+ return undefined;
+ },
+
+ Think() {}
+};
+
+gamemode.set(GM);
+JS_BASE
+
+cat > game/openvibe.games/js/gamemodes/hub/server.js <<'JS_HUB'
+const GM = {
+ mode: "hub",
+ name: "OpenVibe Hub",
+
+ Initialize() {
+ OV.log("Hub Initialize fired");
+ },
+
+ MapInitialize(mapName) {
+ OV.log(`Map initialized: ${mapName}`);
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source JS runtime.");
+ OV.broadcast(`${ply.name()} joined the hub.`);
+ },
+
+ PlayerSpawn(ply) {
+ ply.chat("PlayerSpawn hook fired.");
+ },
+
+ PlayerSay(ply, text) {
+ if (text === "!js") {
+ ply.chat("JavaScript hooks are working.");
+ return false;
+ }
+
+ if (text === "!hp") {
+ ply.chat(`Health: ${ply.health()}`);
+ return false;
+ }
+
+ if (text === "!players") {
+ ply.chat(`Players online: ${OV.players().length}`);
+ return false;
+ }
+
+ return undefined;
+ },
+
+ Think() {}
+};
+
+gamemode.set(GM);
+JS_HUB
+
+cat > game/openvibe.games/js/gamemodes/prophunt/server.js <<'JS_PH'
+const props = ["crate", "barrel", "chair", "bucket"];
+
+function randomProp() {
+ return props[Math.floor(Math.random() * props.length)];
+}
+
+const GM = {
+ mode: "prophunt",
+ name: "OpenVibe Prop Hunt",
+
+ Initialize() {
+ OV.log("Prop Hunt Initialize fired");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Prop Hunt: hide as props or hunt them down.");
+ },
+
+ PlayerSay(ply, text) {
+ if (text === "!prop") {
+ ply.runCommand(`ov_prophunt_disguise ${randomProp()}`);
+ return false;
+ }
+
+ if (text.startsWith("!prop ")) {
+ ply.runCommand(`ov_prophunt_disguise ${text.slice(6).trim()}`);
+ return false;
+ }
+
+ return undefined;
+ },
+
+ Think() {}
+};
+
+gamemode.set(GM);
+JS_PH
+
+cat > game/openvibe.games/js/gamemodes/deathrun/server.js <<'JS_DR'
+const GM = {
+ mode: "deathrun",
+ name: "OpenVibe Deathrun",
+
+ Initialize() {
+ OV.log("Deathrun Initialize fired");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Deathrun: survive the traps and reach the finish.");
+ },
+
+ PlayerSay(ply, text) {
+ if (text === "!finish") {
+ ply.chat("Deathrun finish test.");
+ OV.reward(ply, 50, 100, "deathrun_finish");
+ return false;
+ }
+
+ return undefined;
+ }
+};
+
+gamemode.set(GM);
+JS_DR
+
+cat > game/openvibe.games/js/gamemodes/fortwars/server.js <<'JS_FW'
+const allowed = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);
+
+const GM = {
+ mode: "fortwars",
+ name: "OpenVibe Fort Wars",
+
+ Initialize() {
+ OV.log("Fort Wars Initialize fired");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Fort Wars: build first, fight second.");
+ },
+
+ PlayerSay(ply, text) {
+ if (!text.startsWith("!build ")) return undefined;
+
+ const part = text.slice(7).trim();
+ if (!allowed.has(part)) {
+ ply.chat("Allowed: crate, barrel, pallet, fence, sheet");
+ return false;
+ }
+
+ ply.runCommand(`ov_fortwars_spawn ${part}`);
+ return false;
+ }
+};
+
+gamemode.set(GM);
+JS_FW
+
+cat > game/openvibe.games/js/gamemodes/traitortown/server.js <<'JS_TT'
+const roles = new Map();
+
+const GM = {
+ mode: "traitortown",
+ name: "OpenVibe Traitor Town",
+
+ Initialize() {
+ OV.log("Traitor Town Initialize fired");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Traitor Town: find the traitors before they find you.");
+ roles.set(ply.userId(), "innocent");
+ },
+
+ PlayerSay(ply, text) {
+ if (text === "!role") {
+ ply.chat(`Your role: ${roles.get(ply.userId()) || "none"}`);
+ return false;
+ }
+
+ return undefined;
+ },
+
+ PlayerDeath(victim, attacker) {
+ OV.broadcast(`${victim.name()} died.`);
+ }
+};
+
+gamemode.set(GM);
+JS_TT
+
+echo "[openvibe] writing C++ embedded JS files"
+
+cat > sdk/openvibe/shared/ov_js_runtime.h <<'CPP_RT_H'
+#pragma once
+
+#include "openvibe/third_party/quickjs/quickjs.h"
+
+class COpenVibeJSRuntime
+{
+public:
+ COpenVibeJSRuntime();
+ ~COpenVibeJSRuntime();
+
+ bool Init(bool bServerRealm, const char *pszMode);
+ void Shutdown();
+
+ bool LoadFile(const char *pszPath);
+ bool Eval(const char *pszCode, const char *pszFilename);
+
+ JSValue CallHookRaw(const char *pszHookName, int argc = 0, JSValueConst *argv = nullptr);
+ void CallHookVoid(const char *pszHookName, int argc = 0, JSValueConst *argv = nullptr);
+ bool CallHookBool(const char *pszHookName, bool *pOut, int argc = 0, JSValueConst *argv = nullptr);
+
+ JSContext *Context() { return m_pCtx; }
+ const char *GetMode() const { return m_szMode; }
+
+private:
+ void PrintException(const char *pszWhere);
+ bool LoadCoreFiles();
+ bool LoadGamemode();
+
+ JSRuntime *m_pRuntime = nullptr;
+ JSContext *m_pCtx = nullptr;
+ bool m_bServerRealm = false;
+ char m_szMode[64]{};
+};
+CPP_RT_H
+
+cat > sdk/openvibe/shared/ov_js_runtime.cpp <<'CPP_RT_CPP'
+#include "cbase.h"
+#include "filesystem.h"
+#include "ov_js_runtime.h"
+#include "ov_js_bindings.h"
+
+#include "tier0/memdbgon.h"
+
+static int OV_JSInterruptHandler(JSRuntime *rt, void *opaque)
+{
+ return 0;
+}
+
+COpenVibeJSRuntime::COpenVibeJSRuntime() {}
+
+COpenVibeJSRuntime::~COpenVibeJSRuntime()
+{
+ Shutdown();
+}
+
+bool COpenVibeJSRuntime::Init(bool bServerRealm, const char *pszMode)
+{
+ Shutdown();
+
+ m_bServerRealm = bServerRealm;
+ Q_strncpy(m_szMode, pszMode && pszMode[0] ? pszMode : "hub", sizeof(m_szMode));
+
+ m_pRuntime = JS_NewRuntime();
+ if (!m_pRuntime)
+ return false;
+
+ JS_SetMemoryLimit(m_pRuntime, 16 * 1024 * 1024);
+ JS_SetMaxStackSize(m_pRuntime, 512 * 1024);
+ JS_SetInterruptHandler(m_pRuntime, OV_JSInterruptHandler, this);
+
+ m_pCtx = JS_NewContext(m_pRuntime);
+ if (!m_pCtx)
+ {
+ Shutdown();
+ return false;
+ }
+
+ OVJS_RegisterNativeBindings(m_pCtx, this);
+
+ if (!LoadCoreFiles())
+ return false;
+
+ if (!LoadGamemode())
+ return false;
+
+ return true;
+}
+
+void COpenVibeJSRuntime::Shutdown()
+{
+ if (m_pCtx)
+ {
+ JS_FreeContext(m_pCtx);
+ m_pCtx = nullptr;
+ }
+
+ if (m_pRuntime)
+ {
+ JS_FreeRuntime(m_pRuntime);
+ m_pRuntime = nullptr;
+ }
+}
+
+bool COpenVibeJSRuntime::Eval(const char *pszCode, const char *pszFilename)
+{
+ if (!m_pCtx || !pszCode)
+ return false;
+
+ JSValue result = JS_Eval(
+ m_pCtx,
+ pszCode,
+ Q_strlen(pszCode),
+ pszFilename ? pszFilename : "<openvibe>",
+ JS_EVAL_TYPE_GLOBAL
+ );
+
+ if (JS_IsException(result))
+ {
+ PrintException(pszFilename);
+ JS_FreeValue(m_pCtx, result);
+ return false;
+ }
+
+ JS_FreeValue(m_pCtx, result);
+ return true;
+}
+
+bool COpenVibeJSRuntime::LoadFile(const char *pszPath)
+{
+ FileHandle_t file = filesystem->Open(pszPath, "rb", "MOD");
+ if (!file)
+ {
+ Warning("[OV JS] Could not open %s\n", pszPath);
+ return false;
+ }
+
+ int size = filesystem->Size(file);
+ if (size <= 0 || size > 1024 * 1024)
+ {
+ filesystem->Close(file);
+ Warning("[OV JS] Refusing script %s size=%d\n", pszPath, size);
+ return false;
+ }
+
+ char *buffer = new char[size + 1];
+ filesystem->Read(buffer, size, file);
+ filesystem->Close(file);
+ buffer[size] = '\0';
+
+ bool ok = Eval(buffer, pszPath);
+ delete[] buffer;
+
+ return ok;
+}
+
+bool COpenVibeJSRuntime::LoadCoreFiles()
+{
+ if (!LoadFile("js/core/hook.js"))
+ return false;
+
+ if (!LoadFile("js/core/gamemode.js"))
+ return false;
+
+ if (!LoadFile("js/bridge.js"))
+ return false;
+
+ return true;
+}
+
+bool COpenVibeJSRuntime::LoadGamemode()
+{
+ LoadFile("js/gamemodes/base/server.js");
+
+ char path[256];
+ Q_snprintf(path, sizeof(path), "js/gamemodes/%s/%s.js", m_szMode, m_bServerRealm ? "server" : "client");
+
+ return LoadFile(path);
+}
+
+JSValue COpenVibeJSRuntime::CallHookRaw(const char *pszHookName, int argc, JSValueConst *argv)
+{
+ if (!m_pCtx)
+ return JS_UNDEFINED;
+
+ JSValue global = JS_GetGlobalObject(m_pCtx);
+ JSValue gamemode = JS_GetPropertyStr(m_pCtx, global, "gamemode");
+ JSValue call = JS_GetPropertyStr(m_pCtx, gamemode, "call");
+
+ if (!JS_IsFunction(m_pCtx, call))
+ {
+ JS_FreeValue(m_pCtx, call);
+ JS_FreeValue(m_pCtx, gamemode);
+ JS_FreeValue(m_pCtx, global);
+ return JS_UNDEFINED;
+ }
+
+ JSValue *callArgs = new JSValue[argc + 1];
+ callArgs[0] = JS_NewString(m_pCtx, pszHookName);
+
+ for (int i = 0; i < argc; ++i)
+ callArgs[i + 1] = JS_DupValue(m_pCtx, argv[i]);
+
+ JSValue result = JS_Call(m_pCtx, call, gamemode, argc + 1, callArgs);
+
+ for (int i = 0; i < argc + 1; ++i)
+ JS_FreeValue(m_pCtx, callArgs[i]);
+
+ delete[] callArgs;
+
+ JS_FreeValue(m_pCtx, call);
+ JS_FreeValue(m_pCtx, gamemode);
+ JS_FreeValue(m_pCtx, global);
+
+ if (JS_IsException(result))
+ {
+ PrintException(pszHookName);
+ JS_FreeValue(m_pCtx, result);
+ return JS_UNDEFINED;
+ }
+
+ return result;
+}
+
+void COpenVibeJSRuntime::CallHookVoid(const char *pszHookName, int argc, JSValueConst *argv)
+{
+ if (!m_pCtx)
+ return;
+
+ JSValue result = CallHookRaw(pszHookName, argc, argv);
+ JS_FreeValue(m_pCtx, result);
+}
+
+bool COpenVibeJSRuntime::CallHookBool(const char *pszHookName, bool *pOut, int argc, JSValueConst *argv)
+{
+ if (!m_pCtx)
+ return false;
+
+ JSValue result = CallHookRaw(pszHookName, argc, argv);
+
+ if (JS_IsUndefined(result))
+ {
+ JS_FreeValue(m_pCtx, result);
+ return false;
+ }
+
+ if (pOut)
+ *pOut = JS_ToBool(m_pCtx, result) != 0;
+
+ JS_FreeValue(m_pCtx, result);
+ return true;
+}
+
+void COpenVibeJSRuntime::PrintException(const char *pszWhere)
+{
+ JSValue exception = JS_GetException(m_pCtx);
+ const char *pszError = JS_ToCString(m_pCtx, exception);
+
+ Warning("[OV JS] Exception in %s: %s\n", pszWhere ? pszWhere : "<unknown>", pszError ? pszError : "<null>");
+
+ if (pszError)
+ JS_FreeCString(m_pCtx, pszError);
+
+ JS_FreeValue(m_pCtx, exception);
+}
+CPP_RT_CPP
+
+cat > sdk/openvibe/shared/ov_js_bindings.h <<'CPP_BIND_H'
+#pragma once
+
+#include "openvibe/third_party/quickjs/quickjs.h"
+
+class COpenVibeJSRuntime;
+
+void OVJS_RegisterNativeBindings(JSContext *ctx, COpenVibeJSRuntime *runtime);
+CPP_BIND_H
+
+cat > sdk/openvibe/shared/ov_js_player.h <<'CPP_PLAYER_H'
+#pragma once
+
+#include "openvibe/third_party/quickjs/quickjs.h"
+
+class CHL2MP_Player;
+
+void OVJS_RegisterPlayerClass(JSContext *ctx);
+JSValue OVJS_NewPlayer(JSContext *ctx, CHL2MP_Player *player);
+CHL2MP_Player *OVJS_ResolvePlayerByUserId(int userId);
+CHL2MP_Player *OVJS_GetPlayerFromThis(JSContext *ctx, JSValueConst thisVal);
+CPP_PLAYER_H
+
+cat > sdk/openvibe/shared/ov_js_player.cpp <<'CPP_PLAYER_CPP'
+#include "cbase.h"
+#include "hl2mp_player.h"
+#include "player.h"
+#include "util.h"
+#include "ov_js_player.h"
+
+#include "tier0/memdbgon.h"
+
+struct OVJSPlayerHandle
+{
+ int userId;
+ int entIndex;
+};
+
+static JSClassID g_OVJSPlayerClassID;
+
+CHL2MP_Player *OVJS_ResolvePlayerByUserId(int userId)
+{
+ for (int i = 1; i <= gpGlobals->maxClients; ++i)
+ {
+ CBasePlayer *base = UTIL_PlayerByIndex(i);
+ if (!base)
+ continue;
+
+ CHL2MP_Player *player = ToHL2MPPlayer(base);
+ if (!player)
+ continue;
+
+ if (player->GetUserID() == userId)
+ return player;
+ }
+
+ return nullptr;
+}
+
+static void OVJS_PlayerFinalizer(JSRuntime *rt, JSValue val)
+{
+ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque(val, g_OVJSPlayerClassID));
+ delete handle;
+}
+
+CHL2MP_Player *OVJS_GetPlayerFromThis(JSContext *ctx, JSValueConst thisVal)
+{
+ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));
+ if (!handle)
+ return nullptr;
+
+ CHL2MP_Player *player = OVJS_ResolvePlayerByUserId(handle->userId);
+ if (!player)
+ return nullptr;
+
+ if (player->entindex() != handle->entIndex)
+ return nullptr;
+
+ return player;
+}
+
+static JSValue OVJS_Player_userId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));
+ return handle ? JS_NewInt32(ctx, handle->userId) : JS_UNDEFINED;
+}
+
+static JSValue OVJS_Player_entIndex(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ OVJSPlayerHandle *handle = static_cast<OVJSPlayerHandle *>(JS_GetOpaque2(ctx, thisVal, g_OVJSPlayerClassID));
+ return handle ? JS_NewInt32(ctx, handle->entIndex) : JS_UNDEFINED;
+}
+
+static JSValue OVJS_Player_name(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ return JS_NewString(ctx, player ? player->GetPlayerName() : "");
+}
+
+static JSValue OVJS_Player_steamId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ if (!player)
+ return JS_NewString(ctx, "");
+
+ const char *netid = engine->GetPlayerNetworkIDString(player->edict());
+ return JS_NewString(ctx, netid ? netid : "");
+}
+
+static JSValue OVJS_Player_health(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ return JS_NewInt32(ctx, player ? player->GetHealth() : 0);
+}
+
+static JSValue OVJS_Player_setHealth(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ if (!player || argc < 1)
+ return JS_UNDEFINED;
+
+ int32 value = 0;
+ JS_ToInt32(ctx, &value, argv[0]);
+
+ if (value < 0) value = 0;
+ if (value > 1000) value = 1000;
+
+ player->SetHealth(value);
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_Player_team(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ return JS_NewInt32(ctx, player ? player->GetTeamNumber() : 0);
+}
+
+static JSValue OVJS_Player_setTeam(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ if (!player || argc < 1)
+ return JS_UNDEFINED;
+
+ int32 team = 0;
+ JS_ToInt32(ctx, &team, argv[0]);
+
+ if (team >= 0 && team <= 4)
+ player->ChangeTeam(team);
+
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_Player_chat(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ if (!player || argc < 1)
+ return JS_UNDEFINED;
+
+ const char *msg = JS_ToCString(ctx, argv[0]);
+ if (!msg)
+ return JS_UNDEFINED;
+
+ ClientPrint(player, HUD_PRINTTALK, msg);
+
+ JS_FreeCString(ctx, msg);
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_Player_runCommand(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ CHL2MP_Player *player = OVJS_GetPlayerFromThis(ctx, thisVal);
+ if (!player || argc < 1)
+ return JS_UNDEFINED;
+
+ const char *cmd = JS_ToCString(ctx, argv[0]);
+ if (!cmd)
+ return JS_UNDEFINED;
+
+ if (!Q_strnicmp(cmd, "ov_", 3))
+ engine->ClientCommand(player->edict(), "%s\n", cmd);
+ else
+ Warning("[OV JS] blocked player command: %s\n", cmd);
+
+ JS_FreeCString(ctx, cmd);
+ return JS_UNDEFINED;
+}
+
+static const JSCFunctionListEntry OVJSPlayerProtoFuncs[] =
+{
+ JS_CFUNC_DEF("userId", 0, OVJS_Player_userId),
+ JS_CFUNC_DEF("entIndex", 0, OVJS_Player_entIndex),
+ JS_CFUNC_DEF("steamId", 0, OVJS_Player_steamId),
+ JS_CFUNC_DEF("name", 0, OVJS_Player_name),
+ JS_CFUNC_DEF("health", 0, OVJS_Player_health),
+ JS_CFUNC_DEF("setHealth", 1, OVJS_Player_setHealth),
+ JS_CFUNC_DEF("team", 0, OVJS_Player_team),
+ JS_CFUNC_DEF("setTeam", 1, OVJS_Player_setTeam),
+ JS_CFUNC_DEF("chat", 1, OVJS_Player_chat),
+ JS_CFUNC_DEF("runCommand", 1, OVJS_Player_runCommand),
+};
+
+void OVJS_RegisterPlayerClass(JSContext *ctx)
+{
+ JSRuntime *rt = JS_GetRuntime(ctx);
+
+ JS_NewClassID(&g_OVJSPlayerClassID);
+
+ JSClassDef cls = {};
+ cls.class_name = "OpenVibePlayer";
+ cls.finalizer = OVJS_PlayerFinalizer;
+
+ JS_NewClass(rt, g_OVJSPlayerClassID, &cls);
+
+ JSValue proto = JS_NewObject(ctx);
+ JS_SetPropertyFunctionList(ctx, proto, OVJSPlayerProtoFuncs, ARRAYSIZE(OVJSPlayerProtoFuncs));
+ JS_SetClassProto(ctx, g_OVJSPlayerClassID, proto);
+}
+
+JSValue OVJS_NewPlayer(JSContext *ctx, CHL2MP_Player *player)
+{
+ if (!player)
+ return JS_NULL;
+
+ JSValue obj = JS_NewObjectClass(ctx, g_OVJSPlayerClassID);
+ if (JS_IsException(obj))
+ return obj;
+
+ OVJSPlayerHandle *handle = new OVJSPlayerHandle();
+ handle->userId = player->GetUserID();
+ handle->entIndex = player->entindex();
+
+ JS_SetOpaque(obj, handle);
+ return obj;
+}
+CPP_PLAYER_CPP
+
+cat > sdk/openvibe/shared/ov_js_bindings.cpp <<'CPP_BIND_CPP'
+#include "cbase.h"
+#include "ov_js_bindings.h"
+#include "ov_js_runtime.h"
+#include "ov_js_player.h"
+#include "hl2mp_player.h"
+#include "util.h"
+
+#include "tier0/memdbgon.h"
+
+static COpenVibeJSRuntime *g_OVRuntime = nullptr;
+
+static JSValue OVJS_log(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_UNDEFINED;
+ const char *msg = JS_ToCString(ctx, argv[0]);
+ if (msg) { Msg("[OV JS] %s\n", msg); JS_FreeCString(ctx, msg); }
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_warn(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_UNDEFINED;
+ const char *msg = JS_ToCString(ctx, argv[0]);
+ if (msg) { Warning("[OV JS] %s\n", msg); JS_FreeCString(ctx, msg); }
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_error(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_UNDEFINED;
+ const char *msg = JS_ToCString(ctx, argv[0]);
+ if (msg) { Warning("[OV JS ERROR] %s\n", msg); JS_FreeCString(ctx, msg); }
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_getMode(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ return JS_NewString(ctx, g_OVRuntime ? g_OVRuntime->GetMode() : "unknown");
+}
+
+static JSValue OVJS_getMapName(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ return JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");
+}
+
+static JSValue OVJS_time(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ return JS_NewFloat64(ctx, gpGlobals ? gpGlobals->curtime : 0.0);
+}
+
+static JSValue OVJS_players(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ JSValue arr = JS_NewArray(ctx);
+ uint32 index = 0;
+
+ for (int i = 1; i <= gpGlobals->maxClients; ++i)
+ {
+ CBasePlayer *base = UTIL_PlayerByIndex(i);
+ if (!base) continue;
+
+ CHL2MP_Player *player = ToHL2MPPlayer(base);
+ if (!player) continue;
+
+ JS_SetPropertyUint32(ctx, arr, index++, OVJS_NewPlayer(ctx, player));
+ }
+
+ return arr;
+}
+
+static JSValue OVJS_playerByUserId(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_NULL;
+
+ int32 userId = 0;
+ JS_ToInt32(ctx, &userId, argv[0]);
+
+ CHL2MP_Player *player = OVJS_ResolvePlayerByUserId(userId);
+ return player ? OVJS_NewPlayer(ctx, player) : JS_NULL;
+}
+
+static JSValue OVJS_broadcast(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_UNDEFINED;
+
+ const char *msg = JS_ToCString(ctx, argv[0]);
+ if (!msg) return JS_UNDEFINED;
+
+ UTIL_ClientPrintAll(HUD_PRINTTALK, msg);
+
+ JS_FreeCString(ctx, msg);
+ return JS_UNDEFINED;
+}
+
+static bool OVJS_IsAllowedServerCommand(const char *cmd)
+{
+ return cmd && (!Q_strnicmp(cmd, "ov_", 3) || !Q_strnicmp(cmd, "say ", 4));
+}
+
+static JSValue OVJS_serverCommand(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (argc < 1) return JS_UNDEFINED;
+
+ const char *cmd = JS_ToCString(ctx, argv[0]);
+ if (!cmd) return JS_UNDEFINED;
+
+ if (OVJS_IsAllowedServerCommand(cmd))
+ engine->ServerCommand(UTIL_VarArgs("%s\n", cmd));
+ else
+ Warning("[OV JS] blocked serverCommand: %s\n", cmd);
+
+ JS_FreeCString(ctx, cmd);
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_fireHook(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ if (!g_OVRuntime || argc < 1) return JS_UNDEFINED;
+
+ const char *hookName = JS_ToCString(ctx, argv[0]);
+ if (!hookName) return JS_UNDEFINED;
+
+ JSValue result = g_OVRuntime->CallHookRaw(hookName, argc - 1, argv + 1);
+
+ JS_FreeCString(ctx, hookName);
+ return result;
+}
+
+static JSValue OVJS_reward(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ Warning("[OV JS] OV.reward stubbed; backend reward wiring comes next.\n");
+ return JS_UNDEFINED;
+}
+
+static JSValue OVJS_endMatch(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ Warning("[OV JS] OV.endMatch stubbed; backend match wiring comes next.\n");
+ return JS_UNDEFINED;
+}
+
+static const JSCFunctionListEntry OVFuncs[] =
+{
+ JS_CFUNC_DEF("log", 1, OVJS_log),
+ JS_CFUNC_DEF("warn", 1, OVJS_warn),
+ JS_CFUNC_DEF("error", 1, OVJS_error),
+ JS_CFUNC_DEF("getMode", 0, OVJS_getMode),
+ JS_CFUNC_DEF("getMapName", 0, OVJS_getMapName),
+ JS_CFUNC_DEF("time", 0, OVJS_time),
+ JS_CFUNC_DEF("players", 0, OVJS_players),
+ JS_CFUNC_DEF("playerByUserId", 1, OVJS_playerByUserId),
+ JS_CFUNC_DEF("broadcast", 1, OVJS_broadcast),
+ JS_CFUNC_DEF("serverCommand", 1, OVJS_serverCommand),
+ JS_CFUNC_DEF("fireHook", 1, OVJS_fireHook),
+ JS_CFUNC_DEF("reward", 4, OVJS_reward),
+ JS_CFUNC_DEF("endMatch", 1, OVJS_endMatch),
+};
+
+void OVJS_RegisterNativeBindings(JSContext *ctx, COpenVibeJSRuntime *runtime)
+{
+ g_OVRuntime = runtime;
+
+ OVJS_RegisterPlayerClass(ctx);
+
+ JSValue global = JS_GetGlobalObject(ctx);
+ JSValue ov = JS_NewObject(ctx);
+
+ JS_SetPropertyFunctionList(ctx, ov, OVFuncs, ARRAYSIZE(OVFuncs));
+ JS_SetPropertyStr(ctx, global, "OV", ov);
+
+ JS_FreeValue(ctx, global);
+}
+CPP_BIND_CPP
+
+cat > sdk/openvibe/server/hl2mp/openvibe_js_server.h <<'CPP_JS_SERVER_H'
+#pragma once
+
+class CHL2MP_Player;
+class CBasePlayer;
+class CBaseEntity;
+
+void OpenVibeJS_ServerInit();
+void OpenVibeJS_ServerShutdown();
+void OpenVibeJS_ServerThink();
+
+void OpenVibeJS_Server_PlayerInitialSpawn(CHL2MP_Player *player);
+void OpenVibeJS_Server_PlayerSpawn(CHL2MP_Player *player);
+void OpenVibeJS_Server_PlayerDeath(CHL2MP_Player *victim, CBaseEntity *attacker, CBaseEntity *inflictor);
+void OpenVibeJS_Server_PlayerDisconnected(CHL2MP_Player *player);
+bool OpenVibeJS_Server_PlayerSay(CHL2MP_Player *player, const char *text);
+CPP_JS_SERVER_H
+
+cat > sdk/openvibe/server/hl2mp/openvibe_js_server.cpp <<'CPP_JS_SERVER_CPP'
+#include "cbase.h"
+#include "hl2mp_player.h"
+#include "openvibe_js_server.h"
+#include "openvibe/ov_js_runtime.h"
+#include "openvibe/ov_js_player.h"
+
+#include "tier0/memdbgon.h"
+
+static ConVar ov_mode(
+ "ov_mode",
+ "hub",
+ FCVAR_GAMEDLL,
+ "OpenVibe mode: hub, prophunt, deathrun, fortwars, traitortown." );
+
+static ConVar ov_js_enabled(
+ "ov_js_enabled",
+ "1",
+ FCVAR_GAMEDLL,
+ "Enable OpenVibe JavaScript runtime." );
+
+static COpenVibeJSRuntime g_OVServerJS;
+static bool g_OVServerJSStarted = false;
+
+static bool OpenVibeJS_IsRunning()
+{
+ return ov_js_enabled.GetBool() && g_OVServerJS.Context() != nullptr;
+}
+
+static void OpenVibeJS_EnsureStarted()
+{
+ if (g_OVServerJSStarted)
+ return;
+
+ g_OVServerJSStarted = true;
+
+ if (!ov_js_enabled.GetBool())
+ {
+ Msg("[OV JS] disabled by ov_js_enabled=0\n");
+ return;
+ }
+
+ if (g_OVServerJS.Init(true, ov_mode.GetString()))
+ {
+ Msg("[OV JS] server runtime initialized for mode '%s'\n", ov_mode.GetString());
+ g_OVServerJS.CallHookVoid("Initialize");
+
+ JSContext *ctx = g_OVServerJS.Context();
+ JSValue mapName = JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");
+ JSValueConst argv[] = { mapName };
+ g_OVServerJS.CallHookVoid("MapInitialize", 1, argv);
+ JS_FreeValue(ctx, mapName);
+ }
+ else
+ {
+ Warning("[OV JS] server runtime failed to initialize\n");
+ }
+}
+
+void OpenVibeJS_ServerInit()
+{
+ OpenVibeJS_EnsureStarted();
+}
+
+void OpenVibeJS_ServerShutdown()
+{
+ if (OpenVibeJS_IsRunning())
+ g_OVServerJS.CallHookVoid("Shutdown");
+
+ g_OVServerJS.Shutdown();
+ g_OVServerJSStarted = false;
+}
+
+void OpenVibeJS_ServerThink()
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning())
+ return;
+
+ g_OVServerJS.CallHookVoid("Think");
+}
+
+void OpenVibeJS_Server_PlayerInitialSpawn(CHL2MP_Player *player)
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning() || !player)
+ return;
+
+ JSContext *ctx = g_OVServerJS.Context();
+ JSValue ply = OVJS_NewPlayer(ctx, player);
+
+ JSValueConst argv[] = { ply };
+ g_OVServerJS.CallHookVoid("PlayerInitialSpawn", 1, argv);
+
+ JS_FreeValue(ctx, ply);
+}
+
+void OpenVibeJS_Server_PlayerSpawn(CHL2MP_Player *player)
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning() || !player)
+ return;
+
+ JSContext *ctx = g_OVServerJS.Context();
+ JSValue ply = OVJS_NewPlayer(ctx, player);
+
+ JSValueConst argv[] = { ply };
+ g_OVServerJS.CallHookVoid("PlayerSpawn", 1, argv);
+
+ JS_FreeValue(ctx, ply);
+}
+
+void OpenVibeJS_Server_PlayerDeath(CHL2MP_Player *victim, CBaseEntity *attacker, CBaseEntity *inflictor)
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning() || !victim)
+ return;
+
+ JSContext *ctx = g_OVServerJS.Context();
+
+ JSValue jsVictim = OVJS_NewPlayer(ctx, victim);
+ JSValue jsAttacker = JS_NULL;
+
+ if (attacker && attacker->IsPlayer())
+ jsAttacker = OVJS_NewPlayer(ctx, ToHL2MPPlayer(static_cast<CBasePlayer *>(attacker)));
+
+ JSValueConst argv[] = { jsVictim, jsAttacker };
+ g_OVServerJS.CallHookVoid("PlayerDeath", 2, argv);
+
+ JS_FreeValue(ctx, jsVictim);
+ JS_FreeValue(ctx, jsAttacker);
+}
+
+void OpenVibeJS_Server_PlayerDisconnected(CHL2MP_Player *player)
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning() || !player)
+ return;
+
+ JSContext *ctx = g_OVServerJS.Context();
+ JSValue ply = OVJS_NewPlayer(ctx, player);
+
+ JSValueConst argv[] = { ply };
+ g_OVServerJS.CallHookVoid("PlayerDisconnected", 1, argv);
+
+ JS_FreeValue(ctx, ply);
+}
+
+bool OpenVibeJS_Server_PlayerSay(CHL2MP_Player *player, const char *text)
+{
+ OpenVibeJS_EnsureStarted();
+
+ if (!OpenVibeJS_IsRunning() || !player || !text)
+ return false;
+
+ JSContext *ctx = g_OVServerJS.Context();
+
+ JSValue ply = OVJS_NewPlayer(ctx, player);
+ JSValue msg = JS_NewString(ctx, text);
+
+ JSValueConst argv[] = { ply, msg };
+
+ bool value = false;
+ bool returned = g_OVServerJS.CallHookBool("PlayerSay", &value, 2, argv);
+
+ JS_FreeValue(ctx, ply);
+ JS_FreeValue(ctx, msg);
+
+ return returned && value == false;
+}
+
+static void OV_JSReload_f()
+{
+ OpenVibeJS_ServerShutdown();
+ OpenVibeJS_ServerInit();
+ Msg("[OV JS] reloaded\n");
+}
+
+static ConCommand ov_js_reload(
+ "ov_js_reload",
+ OV_JSReload_f,
+ "Reload OpenVibe JavaScript runtime.",
+ FCVAR_GAMEDLL
+);
+CPP_JS_SERVER_CPP
+
+echo "[openvibe] replacing apply-openvibe-sdk.sh"
+
+backup_file tools/apply-openvibe-sdk.sh
+
+cat > tools/apply-openvibe-sdk.sh <<'APPLY'
+#!/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_tree() {
+ local src="$1"
+ local dst="$2"
+ mkdir -p "$dst"
+ rsync -a --delete "$src/" "$dst/"
+ echo "[openvibe-sdk] copied tree ${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"
+
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_runtime.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_runtime.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_runtime.cpp"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_bindings.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_bindings.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_bindings.cpp"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.h" \
+ "$SDK/src/game/shared/openvibe/ov_js_player.h"
+copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.cpp" \
+ "$SDK/src/game/shared/openvibe/ov_js_player.cpp"
+
+copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.h" \
+ "$SDK/src/game/server/hl2mp/openvibe_js_server.h"
+copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp" \
+ "$SDK/src/game/server/hl2mp/openvibe_js_server.cpp"
+
+copy_tree "$ROOT/sdk/openvibe/third_party/quickjs" \
+ "$SDK/src/game/shared/openvibe/third_party/quickjs"
+
+QJS_C="$SDK/src/game/shared/openvibe/third_party/quickjs/quickjs.c"
+if [[ -f "$QJS_C" ]] && ! grep -q 'OPENVIBE_QUICKJS_CONFIG' "$QJS_C"; then
+ tmp="$(mktemp)"
+ cat > "$tmp" <<'QCFG'
+#ifndef OPENVIBE_QUICKJS_CONFIG
+#define OPENVIBE_QUICKJS_CONFIG
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#ifndef CONFIG_VERSION
+#define CONFIG_VERSION "openvibe"
+#endif
+#endif
+
+QCFG
+ cat "$QJS_C" >> "$tmp"
+ mv "$tmp" "$QJS_C"
+fi
+
+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"
+HL2MP_CLIENTMODE="$SDK/src/game/client/hl2mp/clientmode_hl2mpnormal.cpp"
+GAMEINTERFACE="$SDK/src/game/server/gameinterface.cpp"
+HL2MP_PLAYER="$SDK/src/game/server/hl2mp/hl2mp_player.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/^.*hl2mp\\openvibe_js_server\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_bindings\.cpp.*\n//mg;
+ s/^.*openvibe\\ov_js_player\.cpp.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\quickjs\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libregexp\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libunicode\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\cutils\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\dtoa\.c.*\n//mg;
+ s/^.*openvibe\\third_party\\quickjs\\libbf\.c.*\n//mg;
+ s/(\$File\s+"hl2mp\\hl2mp_player\.h"\n)/$1\t\t\t\$File\t"hl2mp\\openvibe_server.cpp"\n\t\t\t\$File\t"hl2mp\\openvibe_js_server.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_runtime.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_bindings.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_js_player.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\quickjs.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libregexp.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libunicode.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\cutils.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\dtoa.c"\n\t\t\t\$File\t"..\\shared\\openvibe\\third_party\\quickjs\\libbf.c"\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 \ OpenVibe_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"
+
+if ! grep -q 'OpenVibe_OnClientModeInit' "$HL2MP_CLIENTMODE"; then
+ sed -i '/#include "ienginevgui.h"/a void OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"
+ sed -i '/BaseClass::Init();/a \ OpenVibe_OnClientModeInit();' "$HL2MP_CLIENTMODE"
+ echo "[openvibe-sdk] patched clientmode_hl2mpnormal.cpp OpenVibe menu hook"
+fi
+
+perl -0pi -e 's/^[ \t]*tOpenVibe_OnClientModeInit\(\);/ OpenVibe_OnClientModeInit();/m' "$HL2MP_CLIENTMODE"
+
+if [[ -f "$GAMEINTERFACE" ]]; then
+ if ! grep -q 'OpenVibe_OnFrame' "$GAMEINTERFACE"; then
+ sed -i '/CServerGameDLL::GameFrame/i #ifdef HL2MP\nvoid OpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"
+ sed -i '/VPROF( "CServerGameDLL::GameFrame" );/a #ifdef HL2MP\n\tOpenVibe_OnFrame();\n#endif' "$GAMEINTERFACE"
+ echo "[openvibe-sdk] patched gameinterface.cpp frame hook"
+ fi
+
+ if ! grep -q 'OpenVibe_OnClientDisconnect' "$GAMEINTERFACE"; then
+ perl -pi -e 's/void CServerGameClients::ClientDisconnect/#ifdef HL2MP\nvoid OpenVibe_OnClientDisconnect( CBasePlayer *pPlayer );\n#endif\nvoid CServerGameClients::ClientDisconnect/' "$GAMEINTERFACE"
+ perl -0777 -pi -e 's/(void CServerGameClients::ClientDisconnect\( edict_t \*pEdict \)\s*\{\s*extern bool\s+g_fGameOver;\s*CBasePlayer \*player = \( CBasePlayer \* \)CBaseEntity::Instance\( pEdict \);)/$1\n#ifdef HL2MP\n\tif ( player ) { OpenVibe_OnClientDisconnect( player ); }\n#endif/g' "$GAMEINTERFACE"
+ echo "[openvibe-sdk] patched gameinterface.cpp disconnect hook"
+ fi
+fi
+
+if [[ -f "$HL2MP_PLAYER" ]]; then
+ if ! grep -q 'OpenVibe_OnPlayerDeath' "$HL2MP_PLAYER"; then
+ sed -i '/CHL2MP_Player::Event_Killed/i void OpenVibe_OnPlayerDeath( CHL2MP_Player *pPlayer, CBaseEntity *pKiller );' "$HL2MP_PLAYER"
+ sed -i '/CTakeDamageInfo subinfo = info;/i \ OpenVibe_OnPlayerDeath( this, info.GetAttacker() );' "$HL2MP_PLAYER"
+ echo "[openvibe-sdk] patched hl2mp_player.cpp death hook"
+ fi
+
+ if ! grep -q 'OpenVibeJS_Server_PlayerSpawn' "$HL2MP_PLAYER"; then
+ sed -i '/CHL2MP_Player::Spawn/i void OpenVibeJS_Server_PlayerSpawn( CHL2MP_Player *pPlayer );' "$HL2MP_PLAYER"
+ sed -i '0,/BaseClass::Spawn();/s//BaseClass::Spawn();\n\tOpenVibeJS_Server_PlayerSpawn( this );/' "$HL2MP_PLAYER"
+ echo "[openvibe-sdk] patched hl2mp_player.cpp spawn hook"
+ fi
+fi
+
+if [[ -f "$HL2MP_CLIENT" ]] && ! grep -q 'OpenVibeJS_Server_PlayerSay' "$HL2MP_CLIENT"; then
+ sed -i '/void Host_Say/a bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *pPlayer, const char *pszText );' "$HL2MP_CLIENT"
+ python3 - "$HL2MP_CLIENT" <<'PY'
+from pathlib import Path
+import re, sys
+
+p = Path(sys.argv[1])
+s = p.read_text()
+
+# Best-effort patch for common Source SDK 2013 HL2MP Host_Say shape:
+# CHL2MP_Player *client = ...
+# const char *p = args.ArgS();
+if "OpenVibeJS_Server_PlayerSay( client," not in s:
+ s2 = re.sub(
+ r'(const char \*p\s*=\s*args\.ArgS\(\);\s*)',
+ r'\1\n\tif ( client && OpenVibeJS_Server_PlayerSay( client, p ) )\n\t\treturn;\n',
+ s,
+ count=1,
+ )
+ if s2 != s:
+ s = s2
+ print("[openvibe-sdk] patched hl2mp_client.cpp PlayerSay hook")
+ else:
+ print("[openvibe-sdk] WARNING: could not auto-patch Host_Say PlayerSay body")
+p.write_text(s)
+PY
+fi
+
+echo "[openvibe-sdk] Source SDK OpenVibe patch applied"
+APPLY
+
+chmod +x tools/apply-openvibe-sdk.sh
+
+echo "[openvibe] patching sdk/openvibe/server/hl2mp/openvibe_server.cpp"
+
+backup_file sdk/openvibe/server/hl2mp/openvibe_server.cpp
+
+python3 <<'PY'
+from pathlib import Path
+import re
+
+path = Path("sdk/openvibe/server/hl2mp/openvibe_server.cpp")
+s = path.read_text()
+
+if '#include "openvibe_js_server.h"' not in s:
+ # Put it after hl2mp_player include if possible, else after first include.
+ s = re.sub(
+ r'(#include\s+"hl2mp_player\.h"\s*\n)',
+ r'\1#include "openvibe_js_server.h"\n',
+ s,
+ count=1,
+ )
+ if '#include "openvibe_js_server.h"' not in s:
+ s = re.sub(r'(#include\s+".*?"\s*\n)', r'\1#include "openvibe_js_server.h"\n', s, count=1)
+
+def replace_function_body(src, signature_regex, new_body):
+ m = re.search(signature_regex, src)
+ if not m:
+ print(f"[openvibe] WARNING: missing function matching {signature_regex}")
+ return src
+
+ brace = src.find("{", m.end())
+ if brace < 0:
+ print(f"[openvibe] WARNING: missing opening brace for {signature_regex}")
+ return src
+
+ depth = 0
+ end = None
+ for i in range(brace, len(src)):
+ if src[i] == "{":
+ depth += 1
+ elif src[i] == "}":
+ depth -= 1
+ if depth == 0:
+ end = i
+ break
+
+ if end is None:
+ print(f"[openvibe] WARNING: missing closing brace for {signature_regex}")
+ return src
+
+ return src[:brace + 1] + "\n" + new_body.rstrip() + "\n" + src[end:]
+
+s = replace_function_body(
+ s,
+ r'void\s+OpenVibe_OnFrame\s*\(\s*\)',
+ """
+ OpenVibeJS_ServerThink();
+"""
+)
+
+s = replace_function_body(
+ s,
+ r'void\s+OpenVibe_OnClientDisconnect\s*\(\s*CBasePlayer\s*\*\s*pPlayer\s*\)',
+ """
+ CHL2MP_Player *pHL2MP = ToHL2MPPlayer( pPlayer );
+ if ( pHL2MP )
+ OpenVibeJS_Server_PlayerDisconnected( pHL2MP );
+"""
+)
+
+s = replace_function_body(
+ s,
+ r'void\s+OpenVibe_OnPlayerDeath\s*\(\s*CHL2MP_Player\s*\*\s*pPlayer\s*,\s*CBaseEntity\s*\*\s*pKiller\s*\)',
+ """
+ OpenVibeJS_Server_PlayerDeath( pPlayer, pKiller, NULL );
+"""
+)
+
+if "OpenVibeJS_Server_PlayerInitialSpawn( pPlayer );" not in s:
+ m = re.search(r'void\s+OpenVibe_OnClientActive\s*\(\s*CHL2MP_Player\s*\*\s*pPlayer\s*\)', s)
+ if m:
+ brace = s.find("{", m.end())
+ depth = 0
+ end = None
+ for i in range(brace, len(s)):
+ if s[i] == "{":
+ depth += 1
+ elif s[i] == "}":
+ depth -= 1
+ if depth == 0:
+ end = i
+ break
+ if end:
+ s = s[:end] + "\n OpenVibeJS_Server_PlayerInitialSpawn( pPlayer );\n" + s[end:]
+ print("[openvibe] patched OpenVibe_OnClientActive -> PlayerInitialSpawn")
+ else:
+ print("[openvibe] WARNING: could not patch OpenVibe_OnClientActive body")
+ else:
+ print("[openvibe] WARNING: OpenVibe_OnClientActive not found")
+
+path.write_text(s)
+PY
+
+echo "[openvibe] running QuickJS smoke test"
+
+tools/smoke-quickjs.sh
+
+echo
+echo "[openvibe] embedded JS bootstrap files written."
+echo
+echo "Next:"
+echo " tools/apply-openvibe-sdk.sh"
+echo " tools/build-sdk-linux.sh"
+echo " tools/setup-openvibe-bin.sh"
+echo " OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh"
+echo
+echo "Then in server console:"
+echo " ov_js_reload"
+echo
+echo "Then in game:"
+echo " connect 127.0.0.1:27015"
+echo " say !js"
+echo " say !hp"
new file mode 100755
new file mode 100755
@@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+QJS="$ROOT/sdk/openvibe/third_party/quickjs"
+BUILD="$ROOT/.tmp/quickjs-smoke"
+
+rm -rf "$BUILD"
+mkdir -p "$BUILD"
+
+cat > "$BUILD/smoke.c" <<'C'
+#include <stdio.h>
+#include "quickjs.h"
+
+int main(void) {
+ JSRuntime *rt = JS_NewRuntime();
+ JSContext *ctx = JS_NewContext(rt);
+
+ JSValue value = JS_Eval(ctx, "1 + 2 + 3", 9, "<smoke>", JS_EVAL_TYPE_GLOBAL);
+ int32_t out = 0;
+ JS_ToInt32(ctx, &out, value);
+
+ JS_FreeValue(ctx, value);
+ JS_FreeContext(ctx);
+ JS_FreeRuntime(rt);
+
+ printf("quickjs result=%d\n", out);
+ return out == 6 ? 0 : 1;
+}
+C
+
+sources=(
+ "$QJS/quickjs.c"
+ "$QJS/libregexp.c"
+ "$QJS/libunicode.c"
+ "$QJS/cutils.c"
+)
+
+[[ -f "$QJS/dtoa.c" ]] && sources+=("$QJS/dtoa.c")
+[[ -f "$QJS/libbf.c" ]] && sources+=("$QJS/libbf.c")
+
+cc \
+ -std=gnu11 \
+ -D_GNU_SOURCE \
+ -DCONFIG_VERSION=\"openvibe\" \
+ -I"$QJS" \
+ "$BUILD/smoke.c" \
+ "${sources[@]}" \
+ -lm \
+ -ldl \
+ -lpthread \
+ -o "$BUILD/smoke-quickjs"
+
+"$BUILD/smoke-quickjs"
new file mode 100755
new file mode 100755
@@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+DEST="$ROOT/sdk/openvibe/third_party/quickjs"
+TMP="${TMPDIR:-/tmp}/openvibe-quickjs-vendor"
+
+rm -rf "$TMP"
+
+git clone --depth 1 https://github.com/bellard/quickjs.git "$TMP" || \
+git clone --depth 1 https://github.com/quickjs-ng/quickjs.git "$TMP"
+
+rm -rf "$DEST"
+mkdir -p "$DEST"
+
+rsync -av \
+ --include='quickjs.c' \
+ --include='quickjs.h' \
+ --include='quickjs-atom.h' \
+ --include='quickjs-opcode.h' \
+ --include='libregexp.c' \
+ --include='libregexp.h' \
+ --include='libregexp-opcode.h' \
+ --include='libunicode.c' \
+ --include='libunicode.h' \
+ --include='libunicode-table.h' \
+ --include='cutils.c' \
+ --include='cutils.h' \
+ --include='dtoa.c' \
+ --include='dtoa.h' \
+ --include='libbf.c' \
+ --include='libbf.h' \
+ --include='list.h' \
+ --include='LICENSE' \
+ --include='VERSION' \
+ --exclude='*' \
+ "$TMP/" \
+ "$DEST/"
+
+git -C "$TMP" rev-parse HEAD > "$DEST/UPSTREAM_COMMIT"
+
+cat > "$DEST/README.openvibe.md" <<'DOC'
+# QuickJS vendored for OpenVibe: Source
+
+Vendored QuickJS core files for OpenVibe's embedded JavaScript runtime.
+
+Excluded intentionally:
+- qjs.c
+- qjsc.c
+- quickjs-libc.c
+- quickjs-libc.h
+
+Reason:
+OpenVibe embeds QuickJS as a sandboxed script VM. Community scripts should not
+receive raw std/os/filesystem/process APIs.
+DOC
+
+echo "[openvibe] vendored QuickJS into $DEST"
+echo "[openvibe] upstream commit: $(cat "$DEST/UPSTREAM_COMMIT")"