openvibers/source.openvibe.games Commits

Next

CO Copilot committed 29 Jun 2026 · f1b2edcdbe · 30 files, +5685 −194


Changes

ddiff --git a/.tmp/quickjs-smoke/smoke-quickjs b/.tmp/quickjs-smoke/smoke-quickjs
nnew file mode 100755
iindex 0000000..472d2b0
BBinary files /dev/null and b/.tmp/quickjs-smoke/smoke-quickjs differ
ddiff --git a/.tmp/quickjs-smoke/smoke.c b/.tmp/quickjs-smoke/smoke.c
nnew file mode 100644
iindex 0000000..8f9fa9d
---- /dev/null
++++ b/.tmp/quickjs-smoke/smoke.c
@@@ -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;
++}
ddiff --git a/game/openvibe.games/js/bridge.js b/game/openvibe.games/js/bridge.js
iindex a6fc7f9..18b0b88 100644
---- a/game/openvibe.games/js/bridge.js
++++ b/game/openvibe.games/js/bridge.js
@@@ -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");
ddiff --git a/game/openvibe.games/js/gamemodes/base/server.js b/game/openvibe.games/js/gamemodes/base/server.js
iindex 4249c5f..9b5a15e 100644
---- a/game/openvibe.games/js/gamemodes/base/server.js
++++ b/game/openvibe.games/js/gamemodes/base/server.js
@@@ -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);
ddiff --git a/game/openvibe.games/js/gamemodes/deathrun/server.js b/game/openvibe.games/js/gamemodes/deathrun/server.js
iindex 050f07c..8ff1e6c 100644
---- a/game/openvibe.games/js/gamemodes/deathrun/server.js
++++ b/game/openvibe.games/js/gamemodes/deathrun/server.js
@@@ -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;
}
};
ddiff --git a/game/openvibe.games/js/gamemodes/fortwars/server.js b/game/openvibe.games/js/gamemodes/fortwars/server.js
iindex f6e1ad0..f907888 100644
---- a/game/openvibe.games/js/gamemodes/fortwars/server.js
++++ b/game/openvibe.games/js/gamemodes/fortwars/server.js
@@@ -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");
-- }
}
};
ddiff --git a/game/openvibe.games/js/gamemodes/hub/server.js b/game/openvibe.games/js/gamemodes/hub/server.js
iindex 3a4899f..3095bed 100644
---- a/game/openvibe.games/js/gamemodes/hub/server.js
++++ b/game/openvibe.games/js/gamemodes/hub/server.js
@@@ -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);
ddiff --git a/game/openvibe.games/js/gamemodes/prophunt/server.js b/game/openvibe.games/js/gamemodes/prophunt/server.js
iindex 0d1f15c..55a0612 100644
---- a/game/openvibe.games/js/gamemodes/prophunt/server.js
++++ b/game/openvibe.games/js/gamemodes/prophunt/server.js
@@@ -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);
ddiff --git a/game/openvibe.games/js/gamemodes/traitortown/server.js b/game/openvibe.games/js/gamemodes/traitortown/server.js
iindex 1e72460..86fee46 100644
---- a/game/openvibe.games/js/gamemodes/traitortown/server.js
++++ b/game/openvibe.games/js/gamemodes/traitortown/server.js
@@@ -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.`);
}
};
ddiff --git a/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp b/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp
nnew file mode 100644
iindex 0000000..e83d7a7
---- /dev/null
++++ b/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp
@@@ -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
++);
ddiff --git a/sdk/openvibe/server/hl2mp/openvibe_js_server.h b/sdk/openvibe/server/hl2mp/openvibe_js_server.h
nnew file mode 100644
iindex 0000000..b076c2f
---- /dev/null
++++ b/sdk/openvibe/server/hl2mp/openvibe_js_server.h
@@@ -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);
ddiff --git a/sdk/openvibe/server/hl2mp/openvibe_server.cpp b/sdk/openvibe/server/hl2mp/openvibe_server.cpp
iindex 2f3a0c8..ab658ef 100644
---- a/sdk/openvibe/server/hl2mp/openvibe_server.cpp
++++ b/sdk/openvibe/server/hl2mp/openvibe_server.cpp
@@@ -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 )
ddiff --git a/sdk/openvibe/server/hl2mp/openvibe_server.cpp.bak.20260628-200219 b/sdk/openvibe/server/hl2mp/openvibe_server.cpp.bak.20260628-200219
nnew file mode 100644
iindex 0000000..2f3a0c8
---- /dev/null
++++ b/sdk/openvibe/server/hl2mp/openvibe_server.cpp.bak.20260628-200219
@@@ -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 );
ddiff --git a/sdk/openvibe/server/hl2mp/openvibe_server.cpp.bak.20260628-200255 b/sdk/openvibe/server/hl2mp/openvibe_server.cpp.bak.20260628-200255
nnew file mode 100644
iindex 0000000..ab658ef
---- /dev/null
++++ b/sdk/openvibe/server/hl2mp/openvibe_server.cpp.bak.20260628-200255
@@@ -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 );
ddiff --git a/sdk/openvibe/server/hl2mp/openvibe_server.cpp.bak.20260628-200636 b/sdk/openvibe/server/hl2mp/openvibe_server.cpp.bak.20260628-200636
nnew file mode 100644
iindex 0000000..ab658ef
---- /dev/null
++++ b/sdk/openvibe/server/hl2mp/openvibe_server.cpp.bak.20260628-200636
@@@ -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 );
ddiff --git a/sdk/openvibe/shared/ov_js_bindings.cpp b/sdk/openvibe/shared/ov_js_bindings.cpp
nnew file mode 100644
iindex 0000000..7aaaf5b
---- /dev/null
++++ b/sdk/openvibe/shared/ov_js_bindings.cpp
@@@ -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);
++}
ddiff --git a/sdk/openvibe/shared/ov_js_bindings.h b/sdk/openvibe/shared/ov_js_bindings.h
nnew file mode 100644
iindex 0000000..1f3e51b
---- /dev/null
++++ b/sdk/openvibe/shared/ov_js_bindings.h
@@@ -0,0 +1,7 @@
++#pragma once
++
++#include "openvibe/third_party/quickjs/quickjs.h"
++
++class COpenVibeJSRuntime;
++
++void OVJS_RegisterNativeBindings(JSContext *ctx, COpenVibeJSRuntime *runtime);
ddiff --git a/sdk/openvibe/shared/ov_js_player.cpp b/sdk/openvibe/shared/ov_js_player.cpp
nnew file mode 100644
iindex 0000000..6e2a965
---- /dev/null
++++ b/sdk/openvibe/shared/ov_js_player.cpp
@@@ -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;
++}
ddiff --git a/sdk/openvibe/shared/ov_js_player.h b/sdk/openvibe/shared/ov_js_player.h
nnew file mode 100644
iindex 0000000..765e2da
---- /dev/null
++++ b/sdk/openvibe/shared/ov_js_player.h
@@@ -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);
ddiff --git a/sdk/openvibe/shared/ov_js_runtime.cpp b/sdk/openvibe/shared/ov_js_runtime.cpp
iindex 0babaab..c35d33e 100644
---- a/sdk/openvibe/shared/ov_js_runtime.cpp
++++ b/sdk/openvibe/shared/ov_js_runtime.cpp
@@@ -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 char
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()
@@@ -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, JSValueC
JSValue 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, JSValueC
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);
ddiff --git a/sdk/openvibe/shared/ov_js_runtime.h b/sdk/openvibe/shared/ov_js_runtime.h
iindex ca87e42..94fee5c 100644
---- a/sdk/openvibe/shared/ov_js_runtime.h
++++ b/sdk/openvibe/shared/ov_js_runtime.h
@@@ -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]{};
};
ddiff --git a/sdk/openvibe/third_party/quickjs/README.openvibe.md b/sdk/openvibe/third_party/quickjs/README.openvibe.md
nnew file mode 100644
iindex 0000000..d3c9d97
---- /dev/null
++++ b/sdk/openvibe/third_party/quickjs/README.openvibe.md
@@@ -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.
ddiff --git a/tools/apply-openvibe-sdk.sh b/tools/apply-openvibe-sdk.sh
iindex 57dfdd3..d5007d6 100755
---- a/tools/apply-openvibe-sdk.sh
++++ b/tools/apply-openvibe-sdk.sh
@@@ -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"; 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"
++ sed -i '/FinishClientPutInServer( pPlayer );/a \ OpenVibe_OnClientActive( pPlayer );' "$HL2MP_CLIENT"
echo "[openvibe-sdk] patched hl2mp_client.cpp arrival hook"
fi
@@@ -51,9 +114,6 @@ 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"
@@@ -74,6 +134,40 @@ if [[ -f "$HL2MP_PLAYER" ]]; then
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"
ddiff --git a/tools/apply-openvibe-sdk.sh.bak.20260628-200219 b/tools/apply-openvibe-sdk.sh.bak.20260628-200219
nnew file mode 100755
iindex 0000000..57dfdd3
---- /dev/null
++++ b/tools/apply-openvibe-sdk.sh.bak.20260628-200219
@@@ -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"
ddiff --git a/tools/apply-openvibe-sdk.sh.bak.20260628-200255 b/tools/apply-openvibe-sdk.sh.bak.20260628-200255
nnew file mode 100755
iindex 0000000..d2e7a5c
---- /dev/null
++++ b/tools/apply-openvibe-sdk.sh.bak.20260628-200255
@@@ -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"
ddiff --git a/tools/apply-openvibe-sdk.sh.bak.20260628-200636 b/tools/apply-openvibe-sdk.sh.bak.20260628-200636
nnew file mode 100755
iindex 0000000..d5007d6
---- /dev/null
++++ b/tools/apply-openvibe-sdk.sh.bak.20260628-200636
@@@ -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"
ddiff --git a/tools/fix-quickjs-vendor-smoke.sh b/tools/fix-quickjs-vendor-smoke.sh
nnew file mode 100755
iindex 0000000..cc3491c
---- /dev/null
++++ b/tools/fix-quickjs-vendor-smoke.sh
@@@ -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."
ddiff --git a/tools/implement-embedded-js.sh b/tools/implement-embedded-js.sh
nnew file mode 100755
iindex 0000000..1b252c7
---- /dev/null
++++ b/tools/implement-embedded-js.sh
@@@ -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"
ddiff --git a/tools/smoke-quickjs.sh b/tools/smoke-quickjs.sh
nnew file mode 100755
iindex 0000000..75ce1aa
---- /dev/null
++++ b/tools/smoke-quickjs.sh
@@@ -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"
ddiff --git a/tools/vendor-quickjs.sh b/tools/vendor-quickjs.sh
nnew file mode 100755
iindex 0000000..b014f98
---- /dev/null
++++ b/tools/vendor-quickjs.sh
@@@ -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")"