openvibers/source.openvibe.games Commits
Bridge server.so + client.dll to Node runtime over IPC
Changes
@@@ -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;@@@ -11,3 +11,4 @@ ov_mode "sandbox"// No round timer in sandbox.mp_timelimit 0mp_fraglimit 0++ov_js_backend "node"@@@ -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;@@@ -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 };@@@ -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"
@@ -85,6 +85,14 @@ function buildOV() {
@@ -85,6 +85,14 @@ function buildOV() {
// Live game state mirrored from IPC events.
// Live game state mirrored from IPC events.
const state = { map: '', players: {} /* userId -> {userId,name,steamId,...} */ };
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) {
function wrapPlayer(p) {
return {
return {
userId: () => p.userId, entIndex: () => p.entIndex || p.userId,
userId: () => p.userId, entIndex: () => p.entIndex || p.userId,
@@ -194,13 +202,13 @@ function onGameMessage(msg) {
@@ -194,13 +202,13 @@ function onGameMessage(msg) {
break;
break;
case 'net': {
case 'net': {
// client->server (or server->client) net message -> OVNetReceive hook
// 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); } }
if (ctx && ctx.hook) { try { ctx.hook.Run('OVNetReceive', msg.name, msg.payload, ply); } catch (e) { warn(e.message); } }
break;
break;
}
}
case 'say':
case 'say':
if (ctx && ctx.hook) {
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); }
try { ctx.hook.Run('PlayerSay', ply, msg.text); } catch (e) { warn(e.message); }
}
}
break;
break;
@@ -11,3 +11,4 @@ ov_mode "sandbox"
@@ -11,3 +11,4 @@ ov_mode "sandbox"
// No round timer in sandbox.
// No round timer in sandbox.
mp_timelimit 0
mp_timelimit 0
mp_fraglimit 0
mp_fraglimit 0
+ov_js_backend "node"
@@ -1,252 +1,178 @@
@@ -1,252 +1,178 @@
#include "cbase.h"
#include "cbase.h"
#include "openvibe_js_client.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 "usermessages.h"
#include <stdlib.h>
#include <stdlib.h>
#include <string.h>
#include <string.h>
#include "tier0/memdbgon.h"
#include "tier0/memdbgon.h"
-static OVJSCore *g_pClientCore = NULL;
-
static ConVar ov_client_js_enabled(
static ConVar ov_client_js_enabled(
"ov_client_js_enabled", "1", FCVAR_CLIENTDLL,
"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(
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 )
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( name, sizeof( name ) );
msg.ReadString( payload, sizeof( payload ) );
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()
void OpenVibeJS_Client_Init()
{
{
- if ( g_pClientCore ) return;
+ if ( g_bClientBridgeStarted ) return;
+ g_bClientBridgeStarted = true;
if ( !ov_client_js_enabled.GetBool() )
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;
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()
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()
void OpenVibeJS_Client_Think()
{
{
- if ( g_pClientCore )
- ovjs_fire_hook( g_pClientCore, "Think" );
+ g_ClientIPC.Poll();
}
}
-// ---------------------------------------------------------------------------
-// Client concommands.
-// ---------------------------------------------------------------------------
static void OV_ClientJSStatus_f()
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,
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,
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_Shutdown();
OpenVibeJS_Client_Init();
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
class COpenVibeClientJSSystem : public CAutoGameSystemPerFrame
{
{
public:
public:
COpenVibeClientJSSystem() : CAutoGameSystemPerFrame( "COpenVibeClientJSSystem" ) {}
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;
static COpenVibeClientJSSystem g_OpenVibeClientJSSystem;
@@ -123,6 +123,9 @@ static ConCommand ov_round_end(
@@ -123,6 +123,9 @@ static ConCommand ov_round_end(
#include "openvibe_js_server.h"
#include "openvibe_js_server.h"
#include "openvibe/ov_js_runtime.h"
#include "openvibe/ov_js_runtime.h"
#include "openvibe/ov_js_player.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"
#include "tier0/memdbgon.h"
@@ -138,9 +141,117 @@ static ConVar ov_js_enabled(
@@ -138,9 +141,117 @@ static ConVar ov_js_enabled(
FCVAR_GAMEDLL,
FCVAR_GAMEDLL,
"Enable OpenVibe JavaScript runtime." );
"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 COpenVibeJSRuntime g_OVServerJS;
static bool g_OVServerJSStarted = false;
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()
static bool OpenVibeJS_IsRunning()
{
{
return ov_js_enabled.GetBool() && g_OVServerJS.Context() != nullptr;
return ov_js_enabled.GetBool() && g_OVServerJS.Context() != nullptr;
@@ -159,6 +270,15 @@ static void OpenVibeJS_EnsureStarted()
@@ -159,6 +270,15 @@ static void OpenVibeJS_EnsureStarted()
return;
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() ) )
if ( g_OVServerJS.Init( true, ov_mode.GetString() ) )
{
{
Msg( "[OV JS] server runtime initialized for mode '%s'\n", ov_mode.GetString() );
Msg( "[OV JS] server runtime initialized for mode '%s'\n", ov_mode.GetString() );
@@ -213,6 +333,18 @@ void OpenVibeJS_ServerThink()
@@ -213,6 +333,18 @@ void OpenVibeJS_ServerThink()
{
{
OpenVibeJS_EnsureStarted();
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() )
if ( !OpenVibeJS_IsRunning() )
return;
return;
@@ -293,6 +425,20 @@ bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *player, const char *text )
@@ -293,6 +425,20 @@ bool OpenVibeJS_Server_PlayerSay( CHL2MP_Player *player, const char *text )
{
{
OpenVibeJS_EnsureStarted();
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 )
if ( !OpenVibeJS_IsRunning() || !player || !text )
return false;
return false;
@@ -401,6 +547,24 @@ static void OV_Net_f( const CCommand &args )
@@ -401,6 +547,24 @@ static void OV_Net_f( const CCommand &args )
}
}
OpenVibeJS_EnsureStarted();
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() )
if ( !OpenVibeJS_IsRunning() )
return;
return;
@@ -412,7 +576,7 @@ static void OV_Net_f( const CCommand &args )
@@ -412,7 +576,7 @@ static void OV_Net_f( const CCommand &args )
// UTIL_GetCommandClient() is the player who ran the command (null on the
// UTIL_GetCommandClient() is the player who ran the command (null on the
// server console). DON'T TRUST THE CLIENT — net.Receive handlers must
// server console). DON'T TRUST THE CLIENT — net.Receive handlers must
// validate, per the GMod net guide.
// 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;
JSValue ply = player ? OVJS_NewPlayer( ctx, player ) : JS_NULL;
JSValueConst argv[] = { name, payload, ply };
JSValueConst argv[] = { name, payload, ply };
@@ -58,6 +58,10 @@ copy_file "$ROOT/sdk/openvibe/shared/ovjs_core.h" \
@@ -58,6 +58,10 @@ copy_file "$ROOT/sdk/openvibe/shared/ovjs_core.h" \
"$SDK/src/game/shared/openvibe/ovjs_core.h"
"$SDK/src/game/shared/openvibe/ovjs_core.h"
copy_file "$ROOT/sdk/openvibe/shared/ovjs_core.c" \
copy_file "$ROOT/sdk/openvibe/shared/ovjs_core.c" \
"$SDK/src/game/shared/openvibe/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" \
copy_file "$ROOT/sdk/openvibe/server/hl2mp/openvibe_js_server.h" \
"$SDK/src/game/server/hl2mp/openvibe_js_server.h"
"$SDK/src/game/server/hl2mp/openvibe_js_server.h"
@@ -84,10 +88,11 @@ perl -0pi -e '
@@ -84,10 +88,11 @@ perl -0pi -e '
s/^.*hl2mp\\openvibe_js_client\.cpp.*\n//mg;
s/^.*hl2mp\\openvibe_js_client\.cpp.*\n//mg;
s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;
s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;
s/^.*openvibe\\ovjs_core\.c".*\n//mg;
s/^.*openvibe\\ovjs_core\.c".*\n//mg;
+ s/^.*openvibe\\ov_ipc\.cpp.*\n//mg;
s/^.*libquickjs_openvibe(?:\.a)?".*\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"
' "$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
# Register the OVNet usermessage (server->client net library transport) in the
# shared HL2 usermessage table so both realms agree on it.
# shared HL2 usermessage table so both realms agree on it.
@@ -103,6 +108,7 @@ perl -0pi -e '
@@ -103,6 +108,7 @@ perl -0pi -e '
s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;
s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;
s/^.*openvibe\\ov_js_bindings\.cpp.*\n//mg;
s/^.*openvibe\\ov_js_bindings\.cpp.*\n//mg;
s/^.*openvibe\\ov_js_player\.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\\quickjs\.c.*\n//mg;
s/^.*quickjs\\libregexp\.c.*\n//mg;
s/^.*quickjs\\libregexp\.c.*\n//mg;
s/^.*quickjs\\libunicode\.c.*\n//mg;
s/^.*quickjs\\libunicode\.c.*\n//mg;
@@ -110,7 +116,7 @@ perl -0pi -e '
@@ -110,7 +116,7 @@ perl -0pi -e '
s/^.*quickjs\\dtoa\.c.*\n//mg;
s/^.*quickjs\\dtoa\.c.*\n//mg;
s/^.*quickjs\\libbf\.c.*\n//mg;
s/^.*quickjs\\libbf\.c.*\n//mg;
s/^.*libquickjs_openvibe(?:\.a)?".*\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"
' "$SERVER_VPC"
echo "[openvibe-sdk] patched server_hl2mp.vpc"
echo "[openvibe-sdk] patched server_hl2mp.vpc"