openvibers/source.openvibe.games Commits

Bridge server.so + client.dll to Node runtime over IPC

CO Copilot committed 2 Jul 2026 · ee060992b8 · 5 files, +294 −189

Both realms now talk to ov-runtime.js over the ov_ipc TCP JSON bridge.

Server (ov_js_backend "node"): forwards hello/PlayerSay/ov_net/think to
the runtime and applies chat/concmd/runcmd/net(OVNet usermessage)
commands back. Default stays "embedded" (no regression); node mode is
opt-in per cfg (sandbox uses it).

Client: replaced the crashing embedded QuickJS core entirely with the
ov_ipc bridge — client.dll is now a thin bridge (forward OVNet
usermessages, apply chat/net/concmd/menu), no JS engine, no clang-cl
crash. openvibe_js_client.cpp + client VPC drop ovjs_core/quickjs, add
ov_ipc.cpp.

Verified live on Linux end-to-end: SRCDS (node backend) connects to the
runtime; a player "!q" returns the full Q menu, "!spawn crate" issues
ov_fortwars_spawn + "Spawned: Wooden Crate"; npm (leftpad=007) and
hot-reload all working.

Changes

ddiff --git a/engine/openvibe-js-runtime/ov-runtime.js b/engine/openvibe-js-runtime/ov-runtime.js
iindex e9fa859..9022f57 100644
---- a/engine/openvibe-js-runtime/ov-runtime.js
++++ b/engine/openvibe-js-runtime/ov-runtime.js
@@@ -85,6 +85,14 @@ function buildOV() {
// Live game state mirrored from IPC events.
const state = { map: '', players: {} /* userId -> {userId,name,steamId,...} */ };
++// Return a wrapped player, registering a minimal record if we haven't seen a
++// full connect event yet (so chat replies / net handlers always have a target).
++function ensurePlayer(userId, name) {
++ if (userId == null || userId < 0) return null;
++ if (!state.players[userId]) state.players[userId] = { userId, name: name || ('Player' + userId) };
++ else if (name) state.players[userId].name = name;
++ return wrapPlayer(state.players[userId]);
++}
function wrapPlayer(p) {
return {
userId: () => p.userId, entIndex: () => p.entIndex || p.userId,
@@@ -194,13 +202,13 @@ function onGameMessage(msg) {
break;
case 'net': {
// client->server (or server->client) net message -> OVNetReceive hook
-- const ply = msg.userId != null && state.players[msg.userId] ? wrapPlayer(state.players[msg.userId]) : null;
++ const ply = ensurePlayer(msg.userId, msg.name_) ;
if (ctx && ctx.hook) { try { ctx.hook.Run('OVNetReceive', msg.name, msg.payload, ply); } catch (e) { warn(e.message); } }
break;
}
case 'say':
if (ctx && ctx.hook) {
-- const ply = state.players[msg.userId] ? wrapPlayer(state.players[msg.userId]) : null;
++ const ply = ensurePlayer(msg.userId, msg.playerName);
try { ctx.hook.Run('PlayerSay', ply, msg.text); } catch (e) { warn(e.message); }
}
break;
ddiff --git a/game/openvibe.games/cfg/openvibe_sandbox.cfg b/game/openvibe.games/cfg/openvibe_sandbox.cfg
iindex e02198f..ce69ee9 100644
---- a/game/openvibe.games/cfg/openvibe_sandbox.cfg
++++ b/game/openvibe.games/cfg/openvibe_sandbox.cfg
@@@ -11,3 +11,4 @@ ov_mode "sandbox"
// No round timer in sandbox.
mp_timelimit 0
mp_fraglimit 0
++ov_js_backend "node"
ddiff --git a/sdk/openvibe/client/hl2mp/openvibe_js_client.cpp b/sdk/openvibe/client/hl2mp/openvibe_js_client.cpp
iindex b36c16f..4790f07 100644
---- a/sdk/openvibe/client/hl2mp/openvibe_js_client.cpp
++++ b/sdk/openvibe/client/hl2mp/openvibe_js_client.cpp
@@@ -1,252 +1,178 @@
#include "cbase.h"
#include "openvibe_js_client.h"
--// Host-side glue for the client JavaScript runtime. Deliberately includes only
--// the pure C-ABI core header (never quickjs.h), so MSVC cl.exe never parses
--// QuickJS's C headers in C++ mode. All QuickJS work lives in ovjs_core.c
--// (compiled as C / clang-cl on Windows).
--#include "openvibe/ovjs_core.h"
--#include "filesystem.h"
++// Client realm bridge to the OpenVibe Node.js runtime. The client no longer
++// embeds a JS engine (that crashed under clang-cl); instead this forwards engine
++// events to ov-runtime.js over a local TCP socket and applies the commands it
++// sends back. Client-side JS/npm/hooks run in real Node; UI runs in the HTML
++// panel. This is a thin, crash-proof bridge.
++#include "openvibe/ov_ipc.h"
#include "usermessages.h"
#include <stdlib.h>
#include <string.h>
#include "tier0/memdbgon.h"
--static OVJSCore *g_pClientCore = NULL;
--
static ConVar ov_client_js_enabled(
"ov_client_js_enabled", "1", FCVAR_CLIENTDLL,
-- "Enable the OpenVibe client-side JavaScript runtime." );
++ "Enable the OpenVibe client-side Node.js runtime bridge." );
++static ConVar ov_client_js_port(
++ "ov_client_js_port", "41998", FCVAR_CLIENTDLL,
++ "TCP port of the OpenVibe client Node.js runtime host." );
static ConVar ov_client_mode(
-- "ov_client_mode", "hub", FCVAR_CLIENTDLL,
-- "OpenVibe client gamemode realm: hub, prophunt, deathrun, fortwars, traitortown, sandbox." );
++ "ov_client_mode", "sandbox", FCVAR_CLIENTDLL,
++ "OpenVibe client gamemode realm hint sent to the runtime." );
--// ---------------------------------------------------------------------------
--// Host callbacks (C ABI). Source SDK lives entirely on this side.
--// ---------------------------------------------------------------------------
--static bool OVC_IsSafeModPath( const char *p )
--{
-- if ( !p || !p[0] ) return false;
-- if ( p[0] == '/' || p[0] == '\\' ) return false;
-- if ( strstr( p, ".." ) ) return false;
-- if ( strstr( p, ":" ) ) return false;
-- return true;
--}
++static COpenVibeIPC g_ClientIPC;
++static bool g_bClientBridgeStarted = false;
--// Crash-safe logger: Msg() -> console.log is buffered under Proton, so lines
--// right before a crash are lost. Also append each line to a MOD file that we
--// flush (Open/Write/Close) every call, guaranteeing the last line before any
--// crash is on disk. This is the reliable end-to-end client JS log.
--static void OVC_LogLine( const char *tag, const char *m )
++// ---- tiny JSON helpers (build + extract) ----
++static void OVJSON_AppendEscaped( char *dst, int dstSize, const char *src )
{
-- Msg( "[OV JS/client]%s %s\n", tag, m );
-- if ( filesystem )
++ int len = Q_strlen( dst );
++ for ( const char *p = src; p && *p && len < dstSize - 2; ++p )
{
-- FileHandle_t f = filesystem->Open( "ovjs_client.log", "a", "MOD" );
-- if ( f )
-- {
-- char line[8192];
-- int n = Q_snprintf( line, sizeof( line ), "[OV JS/client]%s %s\n", tag, m );
-- if ( n > 0 ) filesystem->Write( line, n, f );
-- filesystem->Close( f ); // close each line = flush to disk
-- }
++ char c = *p;
++ if ( c == '"' || c == '\\' ) { if ( len < dstSize - 3 ) { dst[len++] = '\\'; dst[len++] = c; } }
++ else if ( c == '\n' ) { dst[len++] = ' '; }
++ else if ( c == '\r' ) { /* skip */ }
++ else { dst[len++] = c; }
}
++ dst[len] = '\0';
}
--static void OVC_log( const char *m ) { OVC_LogLine( "", m ); }
--static void OVC_warn( const char *m ) { OVC_LogLine( " WARN", m ); }
--static void OVC_error( const char *m ){ OVC_LogLine( " ERROR", m ); }
--static char *OVC_readFile( const char *path )
++static bool OVJSON_GetString( const char *json, const char *key, char *out, int outSize )
{
-- if ( !OVC_IsSafeModPath( path ) ) return NULL;
-- FileHandle_t f = filesystem->Open( path, "rb", "MOD" );
-- if ( !f ) return NULL;
-- int size = filesystem->Size( f );
-- if ( size < 0 || size > 8 * 1024 * 1024 ) { filesystem->Close( f ); return NULL; }
-- char *buf = (char *)malloc( size + 1 );
-- if ( !buf ) { filesystem->Close( f ); return NULL; }
-- filesystem->Read( buf, size, f );
-- filesystem->Close( f );
-- buf[size] = '\0';
-- return buf;
++ char needle[64];
++ Q_snprintf( needle, sizeof( needle ), "\"%s\":\"", key );
++ const char *s = Q_strstr( json, needle );
++ if ( !s ) return false;
++ s += Q_strlen( needle );
++ int i = 0;
++ while ( *s && *s != '"' && i < outSize - 1 )
++ {
++ if ( *s == '\\' && s[1] ) s++;
++ out[i++] = *s++;
++ }
++ out[i] = '\0';
++ return true;
}
--static int OVC_fileExists( const char *path )
++// ---- inbound: commands from the Node runtime ----
++static void OVClient_OnLine( const char *line )
{
-- return OVC_IsSafeModPath( path )
-- && filesystem->FileExists( path, "MOD" )
-- && !filesystem->IsDirectory( path, "MOD" ) ? 1 : 0;
--}
++ char t[32];
++ if ( !OVJSON_GetString( line, "t", t, sizeof( t ) ) ) return;
--static char *OVC_listDir( const char *dir, const char *wildcard )
--{
-- if ( !OVC_IsSafeModPath( dir ) ) return NULL;
-- char search[512];
-- Q_snprintf( search, sizeof( search ), "%s/%s", dir, ( wildcard && wildcard[0] ) ? wildcard : "*" );
--
-- CUtlString out;
-- FileFindHandle_t h;
-- const char *name = filesystem->FindFirstEx( search, "MOD", &h );
-- bool first = true;
-- while ( name )
++ if ( !Q_strcmp( t, "chat" ) )
++ {
++ char msg[512];
++ if ( OVJSON_GetString( line, "msg", msg, sizeof( msg ) ) )
++ Msg( "[OV] %s\n", msg );
++ }
++ else if ( !Q_strcmp( t, "concmd" ) || !Q_strcmp( t, "runcmd" ) )
{
-- if ( Q_strcmp( name, "." ) != 0 && Q_strcmp( name, ".." ) != 0 )
++ char cmd[1024];
++ if ( OVJSON_GetString( line, "cmd", cmd, sizeof( cmd ) ) )
{
-- if ( !first ) out += "\n";
-- out += name;
-- first = false;
++ char full[1100];
++ Q_snprintf( full, sizeof( full ), "%s\n", cmd );
++ engine->ClientCmd_Unrestricted( full );
}
-- name = filesystem->FindNext( h );
}
-- filesystem->FindClose( h );
--
-- const char *s = out.Get();
-- if ( !s || !s[0] ) return NULL;
-- size_t len = strlen( s );
-- char *buf = (char *)malloc( len + 1 );
-- if ( !buf ) return NULL;
-- memcpy( buf, s, len + 1 );
-- return buf;
--}
--
--// Free buffers OVC_readFile/OVC_listDir allocated, using THIS module's CRT
--// (must match the malloc that created them; the core must not free them).
--static void OVC_freeMem( void *p ) { free( p ); }
--
--static int OVC_isServer( void ) { return 0; }
--static const char *OVC_getMode( void ) { return ov_client_mode.GetString(); }
--
--static const char *OVC_getMapName( void )
--{
-- static char s_map[128];
-- const char *lvl = engine ? engine->GetLevelName() : "";
-- Q_strncpy( s_map, lvl ? lvl : "", sizeof( s_map ) );
-- return s_map;
--}
--
--static double OVC_getTime( void ) { return gpGlobals ? gpGlobals->curtime : 0.0; }
--
--static void OVC_netSendToServer( const char *name, const char *payloadB64 )
--{
-- if ( !name || !payloadB64 || !engine ) return;
-- char cmd[16384];
-- Q_snprintf( cmd, sizeof( cmd ), "ov_net %s %s\n", name, payloadB64 );
-- engine->ClientCmd_Unrestricted( cmd );
--}
--
--static void OVC_netEmit( const char *, const char *, const char * )
--{
-- // server->client emit is server-only; inert on the client.
++ else if ( !Q_strcmp( t, "net" ) )
++ {
++ // client runtime -> server: forward as ov_net
++ char name[128], payload[8192];
++ if ( OVJSON_GetString( line, "name", name, sizeof( name ) ) &&
++ OVJSON_GetString( line, "payload", payload, sizeof( payload ) ) )
++ {
++ char cmd[9000];
++ Q_snprintf( cmd, sizeof( cmd ), "ov_net %s %s\n", name, payload );
++ engine->ClientCmd_Unrestricted( cmd );
++ }
++ }
++ else if ( !Q_strcmp( t, "menu" ) )
++ {
++ char action[32];
++ OVJSON_GetString( line, "action", action, sizeof( action ) );
++ engine->ClientCmd_Unrestricted( !Q_strcmp( action, "close" ) ? "ov_menu_close\n" : "ov_menu\n" );
++ }
}
--static OVJSHost g_ClientHost =
--{
-- OVC_log, OVC_warn, OVC_error,
-- OVC_readFile, OVC_fileExists, OVC_listDir, OVC_freeMem,
-- OVC_isServer, OVC_getMode, OVC_getMapName, OVC_getTime,
-- OVC_netSendToServer, OVC_netEmit
--};
--
--// ---------------------------------------------------------------------------
--// server -> client: OVNet usermessage -> OVNetReceive hook into client JS.
--// ---------------------------------------------------------------------------
++// ---- outbound: server->client net usermessage -> runtime ----
void __MsgFunc_OVNet( bf_read &msg )
{
-- if ( !g_pClientCore ) return;
-- char name[128];
-- char payload[16384];
++ if ( !g_ClientIPC.IsConnected() ) return;
++ char name[128]; char payload[8192];
msg.ReadString( name, sizeof( name ) );
msg.ReadString( payload, sizeof( payload ) );
-- // ply arg is null on the client (server->client has no sending player).
-- ovjs_fire_hook_s( g_pClientCore, "OVNetReceive", name, payload, NULL );
++ char out[9000] = "{\"t\":\"net\",\"name\":\"";
++ OVJSON_AppendEscaped( out, sizeof( out ), name );
++ Q_strncat( out, "\",\"payload\":\"", sizeof( out ), COPY_ALL_CHARACTERS );
++ OVJSON_AppendEscaped( out, sizeof( out ), payload );
++ Q_strncat( out, "\"}", sizeof( out ), COPY_ALL_CHARACTERS );
++ g_ClientIPC.SendLine( out );
}
--// ---------------------------------------------------------------------------
--// Lifecycle.
--// ---------------------------------------------------------------------------
++// ---- lifecycle ----
void OpenVibeJS_Client_Init()
{
-- if ( g_pClientCore ) return;
++ if ( g_bClientBridgeStarted ) return;
++ g_bClientBridgeStarted = true;
if ( !ov_client_js_enabled.GetBool() )
{
-- Msg( "[OV JS/client] disabled by ov_client_js_enabled=0\n" );
++ Msg( "[OV JS/client] bridge disabled by ov_client_js_enabled=0\n" );
return;
}
-- // Fresh crash-safe log each init.
-- if ( filesystem ) { FileHandle_t f = filesystem->Open( "ovjs_client.log", "w", "MOD" ); if ( f ) filesystem->Close( f ); }
-- g_pClientCore = ovjs_create( &g_ClientHost, 0 /*client*/, ov_client_mode.GetString() );
-- if ( g_pClientCore )
-- {
-- Msg( "[OV JS/client] client runtime initialized for mode '%s'\n", ov_client_mode.GetString() );
-- ovjs_fire_hook( g_pClientCore, "Initialize" );
-- }
-- else
-- {
-- Warning( "[OV JS/client] client runtime failed to initialize\n" );
-- }
++ g_ClientIPC.Configure( "127.0.0.1", ov_client_js_port.GetInt(), OVClient_OnLine );
++ g_ClientIPC.Poll(); // attempt initial connect
++
++ char hello[256];
++ Q_snprintf( hello, sizeof( hello ), "{\"t\":\"hello\",\"realm\":\"client\",\"mode\":\"%s\",\"map\":\"%s\"}",
++ ov_client_mode.GetString(), engine ? engine->GetLevelName() : "" );
++ g_ClientIPC.SendLine( hello );
++ Msg( "[OV JS/client] Node runtime bridge started (port %d)\n", ov_client_js_port.GetInt() );
}
void OpenVibeJS_Client_Shutdown()
{
-- if ( g_pClientCore )
-- {
-- ovjs_fire_hook( g_pClientCore, "Shutdown" );
-- ovjs_destroy( g_pClientCore );
-- g_pClientCore = NULL;
-- }
++ if ( g_ClientIPC.IsConnected() ) g_ClientIPC.SendLine( "{\"t\":\"bye\"}" );
++ g_ClientIPC.Close();
++ g_bClientBridgeStarted = false;
}
void OpenVibeJS_Client_Think()
{
-- if ( g_pClientCore )
-- ovjs_fire_hook( g_pClientCore, "Think" );
++ g_ClientIPC.Poll();
}
--// ---------------------------------------------------------------------------
--// Client concommands.
--// ---------------------------------------------------------------------------
static void OV_ClientJSStatus_f()
{
-- Msg( "[OV JS/client] enabled=%d running=%d mode=%s\n",
++ Msg( "[OV JS/client] enabled=%d connected=%d port=%d mode=%s\n",
ov_client_js_enabled.GetBool() ? 1 : 0,
-- g_pClientCore ? 1 : 0,
-- ov_client_mode.GetString() );
++ g_ClientIPC.IsConnected() ? 1 : 0,
++ ov_client_js_port.GetInt(), ov_client_mode.GetString() );
}
static ConCommand ov_client_js_status( "ov_client_js_status", OV_ClientJSStatus_f,
-- "Print OpenVibe client JavaScript runtime status.", FCVAR_CLIENTDLL );
++ "Print OpenVibe client Node runtime bridge status.", FCVAR_CLIENTDLL );
--static void OV_ClientJSReload_f()
++static void OV_ClientJSReconnect_f()
{
OpenVibeJS_Client_Shutdown();
OpenVibeJS_Client_Init();
-- Msg( "[OV JS/client] reloaded\n" );
}
--static ConCommand ov_client_js_reload( "ov_client_js_reload", OV_ClientJSReload_f,
-- "Reload the OpenVibe client JavaScript runtime.", FCVAR_CLIENTDLL );
++static ConCommand ov_client_js_reconnect( "ov_client_js_reconnect", OV_ClientJSReconnect_f,
++ "Reconnect the OpenVibe client Node runtime bridge.", FCVAR_CLIENTDLL );
--// Drives the runtime: hook OVNet at load, start on level enter, Think per frame.
++// Drives the bridge: hook OVNet, connect on level enter, poll each frame.
class COpenVibeClientJSSystem : public CAutoGameSystemPerFrame
{
public:
COpenVibeClientJSSystem() : CAutoGameSystemPerFrame( "COpenVibeClientJSSystem" ) {}
-- bool Init() OVERRIDE
-- {
-- usermessages->HookMessage( "OVNet", __MsgFunc_OVNet );
-- return true;
-- }
-- void LevelInitPostEntity() OVERRIDE
-- {
-- OpenVibeJS_Client_Shutdown();
-- OpenVibeJS_Client_Init();
-- }
-- void LevelShutdownPostEntity() OVERRIDE
-- {
-- OpenVibeJS_Client_Shutdown();
-- }
-- void Update( float frametime ) OVERRIDE
-- {
-- OpenVibeJS_Client_Think();
-- }
++ bool Init() OVERRIDE { usermessages->HookMessage( "OVNet", __MsgFunc_OVNet ); return true; }
++ void LevelInitPostEntity() OVERRIDE { OpenVibeJS_Client_Shutdown(); OpenVibeJS_Client_Init(); }
++ void LevelShutdownPostEntity() OVERRIDE { OpenVibeJS_Client_Shutdown(); }
++ void Update( float ) OVERRIDE { OpenVibeJS_Client_Think(); }
};
static COpenVibeClientJSSystem g_OpenVibeClientJSSystem;
ddiff --git a/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp b/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp
iindex 71ec95f..78a74c6 100644
---- a/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp
++++ b/sdk/openvibe/server/hl2mp/openvibe_js_server.cpp
@@@ -123,6 +123,9 @@ static ConCommand ov_round_end(
#include "openvibe_js_server.h"
#include "openvibe/ov_js_runtime.h"
#include "openvibe/ov_js_player.h"
++#include "openvibe/ov_ipc.h"
++#include "recipientfilter.h"
++#include "openvibe/third_party/quickjs/quickjs.h"
#include "tier0/memdbgon.h"
@@@ -138,9 +141,117 @@ static ConVar ov_js_enabled(
FCVAR_GAMEDLL,
"Enable OpenVibe JavaScript runtime." );
++// Backend for the server JS framework: "embedded" = in-process QuickJS (default,
++// always works on Linux); "node" = forward events to the Node.js runtime host
++// (ov-runtime.js) over IPC for full npm + hot-reload.
++static ConVar ov_js_backend(
++ "ov_js_backend", "embedded", FCVAR_GAMEDLL,
++ "OpenVibe server JS backend: embedded | node" );
++static ConVar ov_js_node_port(
++ "ov_js_node_port", "41999", FCVAR_GAMEDLL,
++ "TCP port of the OpenVibe server Node.js runtime host." );
++
static COpenVibeJSRuntime g_OVServerJS;
static bool g_OVServerJSStarted = false;
++// ---------------------------------------------------------------------------
++// Node.js runtime bridge (server realm). Active when ov_js_backend == "node".
++// ---------------------------------------------------------------------------
++static COpenVibeIPC g_ServerIPC;
++static bool OVServer_UseNode() { return !Q_stricmp( ov_js_backend.GetString(), "node" ); }
++
++static bool OVJSONGetStr( const char *json, const char *key, char *out, int outSize )
++{
++ char needle[64]; Q_snprintf( needle, sizeof( needle ), "\"%s\":\"", key );
++ const char *s = Q_strstr( json, needle );
++ if ( !s ) return false;
++ s += Q_strlen( needle );
++ int i = 0;
++ while ( *s && *s != '"' && i < outSize - 1 ) { if ( *s == '\\' && s[1] ) s++; out[i++] = *s++; }
++ out[i] = '\0';
++ return true;
++}
++static int OVJSONGetInt( const char *json, const char *key, int def )
++{
++ char needle[64]; Q_snprintf( needle, sizeof( needle ), "\"%s\":", key );
++ const char *s = Q_strstr( json, needle );
++ if ( !s ) return def;
++ return atoi( s + Q_strlen( needle ) );
++}
++static void OVJSONAppendEsc( char *dst, int cap, const char *src )
++{
++ int len = Q_strlen( dst );
++ for ( const char *p = src; p && *p && len < cap - 3; ++p )
++ {
++ char c = *p;
++ if ( c == '"' || c == '\\' ) { dst[len++] = '\\'; dst[len++] = c; }
++ else if ( c == '\n' || c == '\r' ) { dst[len++] = ' '; }
++ else dst[len++] = c;
++ }
++ dst[len] = '\0';
++}
++
++// Apply a command line from the Node runtime.
++static void OVServer_OnLine( const char *line )
++{
++ char t[32];
++ if ( !OVJSONGetStr( line, "t", t, sizeof( t ) ) ) return;
++
++ if ( !Q_strcmp( t, "chat" ) )
++ {
++ char msg[512]; if ( !OVJSONGetStr( line, "msg", msg, sizeof( msg ) ) ) return;
++ int uid = OVJSONGetInt( line, "userId", -1 );
++ if ( uid >= 0 ) { CBasePlayer *p = UTIL_PlayerByUserId( uid ); if ( p ) ClientPrint( p, HUD_PRINTTALK, msg ); }
++ else UTIL_ClientPrintAll( HUD_PRINTTALK, msg );
++ }
++ else if ( !Q_strcmp( t, "concmd" ) )
++ {
++ char cmd[512]; if ( OVJSONGetStr( line, "cmd", cmd, sizeof( cmd ) ) ) engine->ServerCommand( UTIL_VarArgs( "%s\n", cmd ) );
++ }
++ else if ( !Q_strcmp( t, "runcmd" ) )
++ {
++ // Run an ov_* command as a specific player (e.g. ov_fortwars_spawn crate).
++ char cmd[512]; int uid = OVJSONGetInt( line, "userId", -1 );
++ if ( uid >= 0 && OVJSONGetStr( line, "cmd", cmd, sizeof( cmd ) ) )
++ {
++ CBasePlayer *p = UTIL_PlayerByUserId( uid );
++ if ( p ) { engine->ClientCommand( p->edict(), "%s", cmd ); }
++ }
++ }
++ else if ( !Q_strcmp( t, "net" ) )
++ {
++ // server -> client net: OVNet usermessage. ids: CSV of userIds, -1 = all.
++ char ids[256], name[128], payload[8192];
++ OVJSONGetStr( line, "ids", ids, sizeof( ids ) );
++ if ( !OVJSONGetStr( line, "name", name, sizeof( name ) ) ) return;
++ OVJSONGetStr( line, "payload", payload, sizeof( payload ) );
++ bool broadcast = ( Q_strstr( ids, "-1" ) != NULL ) || ids[0] == '\0';
++ if ( broadcast )
++ {
++ CBroadcastRecipientFilter f;
++ UserMessageBegin( f, "OVNet" ); WRITE_STRING( name ); WRITE_STRING( payload ); MessageEnd();
++ }
++ else
++ {
++ for ( char *tok = strtok( ids, "," ); tok; tok = strtok( NULL, "," ) )
++ {
++ CBasePlayer *p = UTIL_PlayerByUserId( atoi( tok ) );
++ if ( !p ) continue;
++ CSingleUserRecipientFilter f( p );
++ UserMessageBegin( f, "OVNet" ); WRITE_STRING( name ); WRITE_STRING( payload ); MessageEnd();
++ }
++ }
++ }
++}
++
++static void OVServer_SendHello()
++{
++ char hello[256];
++ Q_snprintf( hello, sizeof( hello ), "{\"t\":\"hello\",\"realm\":\"server\",\"mode\":\"%s\",\"map\":\"%s\"}",
++ ov_mode.GetString(), gpGlobals ? STRING( gpGlobals->mapname ) : "" );
++ g_ServerIPC.SendLine( hello );
++}
++
static bool OpenVibeJS_IsRunning()
{
return ov_js_enabled.GetBool() && g_OVServerJS.Context() != nullptr;
@@@ -159,6 +270,15 @@ static void OpenVibeJS_EnsureStarted()
return;
}
++ if ( OVServer_UseNode() )
++ {
++ g_ServerIPC.Configure( "127.0.0.1", ov_js_node_port.GetInt(), OVServer_OnLine );
++ g_ServerIPC.Poll();
++ OVServer_SendHello();
++ Msg( "[OV JS] server backend=node (runtime bridge port %d)\n", ov_js_node_port.GetInt() );
++ return; // embedded QuickJS not used in node mode
++ }
++
if ( g_OVServerJS.Init( true, ov_mode.GetString() ) )
{
Msg( "[OV JS] server runtime initialized for mode '%s'\n", ov_mode.GetString() );
@@@ -213,6 +333,18 @@ void OpenVibeJS_ServerThink()
{
OpenVibeJS_EnsureStarted();
++ if ( OVServer_UseNode() )
++ {
++ g_ServerIPC.Poll();
++ static float s_flNextThink = 0.0f;
++ if ( gpGlobals && gpGlobals->curtime >= s_flNextThink )
++ {
++ s_flNextThink = gpGlobals->curtime + 0.1f; // 10Hz think to Node
++ if ( g_ServerIPC.IsConnected() ) g_ServerIPC.SendLine( "{\"t\":\"think\"}" );
++ }
++ return;
++ }
++
if ( !OpenVibeJS_IsRunning() )
return;
@@@ -293,6 +425,20 @@ bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *player, const char *text )
{
OpenVibeJS_EnsureStarted();
++ if ( OVServer_UseNode() )
++ {
++ if ( !player || !text || !g_ServerIPC.IsConnected() ) return false;
++ char out[1024] = "{\"t\":\"say\",\"userId\":";
++ char num[16]; Q_snprintf( num, sizeof( num ), "%d", player->GetUserID() );
++ Q_strncat( out, num, sizeof( out ), COPY_ALL_CHARACTERS );
++ Q_strncat( out, ",\"text\":\"", sizeof( out ), COPY_ALL_CHARACTERS );
++ OVJSONAppendEsc( out, sizeof( out ), text );
++ Q_strncat( out, "\"}", sizeof( out ), COPY_ALL_CHARACTERS );
++ g_ServerIPC.SendLine( out );
++ // Chat commands (leading '!') are handled by JS; suppress them from public chat.
++ return text[0] == '!';
++ }
++
if ( !OpenVibeJS_IsRunning() || !player || !text )
return false;
@@@ -401,6 +547,24 @@ static void OV_Net_f( const CCommand &args )
}
OpenVibeJS_EnsureStarted();
++
++ CHL2MP_Player *cmdPlayer = ToHL2MPPlayer( UTIL_GetCommandClient() );
++
++ if ( OVServer_UseNode() )
++ {
++ if ( !g_ServerIPC.IsConnected() ) return;
++ char out[9000] = "{\"t\":\"net\",\"userId\":";
++ char num[16]; Q_snprintf( num, sizeof( num ), "%d", cmdPlayer ? cmdPlayer->GetUserID() : -1 );
++ Q_strncat( out, num, sizeof( out ), COPY_ALL_CHARACTERS );
++ Q_strncat( out, ",\"name\":\"", sizeof( out ), COPY_ALL_CHARACTERS );
++ OVJSONAppendEsc( out, sizeof( out ), args[1] );
++ Q_strncat( out, "\",\"payload\":\"", sizeof( out ), COPY_ALL_CHARACTERS );
++ OVJSONAppendEsc( out, sizeof( out ), args[2] );
++ Q_strncat( out, "\"}", sizeof( out ), COPY_ALL_CHARACTERS );
++ g_ServerIPC.SendLine( out );
++ return;
++ }
++
if ( !OpenVibeJS_IsRunning() )
return;
@@@ -412,7 +576,7 @@ static void OV_Net_f( const CCommand &args )
// UTIL_GetCommandClient() is the player who ran the command (null on the
// server console). DON'T TRUST THE CLIENT — net.Receive handlers must
// validate, per the GMod net guide.
-- CHL2MP_Player *player = ToHL2MPPlayer( UTIL_GetCommandClient() );
++ CHL2MP_Player *player = cmdPlayer;
JSValue ply = player ? OVJS_NewPlayer( ctx, player ) : JS_NULL;
JSValueConst argv[] = { name, payload, ply };
ddiff --git a/tools/apply-openvibe-sdk.sh b/tools/apply-openvibe-sdk.sh
iindex 618791e..b424426 100755
---- a/tools/apply-openvibe-sdk.sh
++++ b/tools/apply-openvibe-sdk.sh
@@@ -58,6 +58,10 @@ copy_file "$ROOT/sdk/openvibe/shared/ovjs_core.h" \
"$SDK/src/game/shared/openvibe/ovjs_core.h"
copy_file "$ROOT/sdk/openvibe/shared/ovjs_core.c" \
"$SDK/src/game/shared/openvibe/ovjs_core.c"
++copy_file "$ROOT/sdk/openvibe/shared/ov_ipc.h" \
++ "$SDK/src/game/shared/openvibe/ov_ipc.h"
++copy_file "$ROOT/sdk/openvibe/shared/ov_ipc.cpp" \
++ "$SDK/src/game/shared/openvibe/ov_ipc.cpp"
copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.h" \
"$SDK/src/game/server/hl2mp/openvibe_js_server.h"
@@@ -84,10 +88,11 @@ perl -0pi -e '
s/^.*hl2mp\\openvibe_js_client\.cpp.*\n//mg;
s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;
s/^.*openvibe\\ovjs_core\.c".*\n//mg;
++ s/^.*openvibe\\ov_ipc\.cpp.*\n//mg;
s/^.*libquickjs_openvibe(?:\.a)?".*\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\t\t\t\$File\t"hl2mp\\openvibe_js_client.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ovjs_core.c" [\$LINUXALL]\n\t\t\t\$Lib\t"..\\shared\\openvibe\\third_party\\quickjs\\build\\libquickjs_openvibe"\n/s;
++ 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\t\t\t\$File\t"hl2mp\\openvibe_js_client.cpp"\n\t\t\t\$File\t"..\\shared\\openvibe\\ov_ipc.cpp"\n/s;
' "$CLIENT_VPC"
--echo "[openvibe-sdk] patched client_hl2mp.vpc"
++echo "[openvibe-sdk] patched client_hl2mp.vpc (Node IPC bridge; no embedded QuickJS)"
# Register the OVNet usermessage (server->client net library transport) in the
# shared HL2 usermessage table so both realms agree on it.
@@@ -103,6 +108,7 @@ perl -0pi -e '
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\\ov_ipc\.cpp.*\n//mg;
s/^.*quickjs\\quickjs\.c.*\n//mg;
s/^.*quickjs\\libregexp\.c.*\n//mg;
s/^.*quickjs\\libunicode\.c.*\n//mg;
@@@ -110,7 +116,7 @@ perl -0pi -e '
s/^.*quickjs\\dtoa\.c.*\n//mg;
s/^.*quickjs\\libbf\.c.*\n//mg;
s/^.*libquickjs_openvibe(?:\.a)?".*\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\$Lib\t"..\\shared\\openvibe\\third_party\\quickjs\\build\\libquickjs_openvibe"\n/s;
++ 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\\ov_ipc.cpp"\n\t\t\t\$Lib\t"..\\shared\\openvibe\\third_party\\quickjs\\build\\libquickjs_openvibe"\n/s;
' "$SERVER_VPC"
echo "[openvibe-sdk] patched server_hl2mp.vpc"