openvibers/source.openvibe.games Commits
client JS: split into C ABI core (clang-cl) + C++ glue (no quickjs.h)
Changes
@@@ -1,260 +1,142 @@#include "cbase.h"#include "openvibe_js_client.h"--// The client always uses the real QuickJS runtime (the Windows *server* stub--// guard keys on GAME_DLL, which is not defined in the client build).++// 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"--#include "openvibe/ov_js_runtime.h"--#include "openvibe/ov_js_bindings.h"#include "usermessages.h"--#include <ctype.h>++#include <stdlib.h>++#include <string.h>#include "tier0/memdbgon.h"--// -----------------------------------------------------------------------------// Client JS runtime instance + mode.--// -----------------------------------------------------------------------------static COpenVibeJSRuntime g_OVClientJS;--static bool g_OVClientJSStarted = false;++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." );----// Which gamemode's client.js to load. The server pushes its mode to the client--// (ov_client_mode <mode>; then the runtime reloads), defaulting to hub.static ConVar ov_client_mode("ov_client_mode", "hub", FCVAR_CLIENTDLL,"OpenVibe client gamemode realm: hub, prophunt, deathrun, fortwars, traitortown, sandbox." );--static bool OpenVibeClientJS_IsRunning()--{-- return ov_client_js_enabled.GetBool() && g_OVClientJS.Context() != nullptr;--}--// -----------------------------------------------------------------------------// Client OV.* native bindings. Client-safe subset (no server player list /--// ServerCommand). File I/O + logging + net + realm info, matching the server--// bridge so the shared core JS (require/net/addon/timer/hook) runs unchanged.++// Host callbacks (C ABI). Source SDK lives entirely on this side.// -----------------------------------------------------------------------------static bool OVCJS_IsSafeModPath( const char *pszPath )++static bool OVC_IsSafeModPath( const char *p ){-- if ( !pszPath || !pszPath[0] ) return false;-- if ( pszPath[0] == '/' || pszPath[0] == '\\' ) return false;-- if ( Q_strstr( pszPath, ".." ) ) return false;-- if ( Q_strstr( pszPath, ":" ) ) return false;++ 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 JSValue OVCJS_log( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )--{-- if ( argc < 1 ) return JS_UNDEFINED;-- const char *msg = JS_ToCString( ctx, argv[0] );-- if ( msg ) { Msg( "[OV JS/client] %s\n", msg ); JS_FreeCString( ctx, msg ); }-- return JS_UNDEFINED;--}----static JSValue OVCJS_warn( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )--{-- if ( argc < 1 ) return JS_UNDEFINED;-- const char *msg = JS_ToCString( ctx, argv[0] );-- if ( msg ) { Warning( "[OV JS/client] %s\n", msg ); JS_FreeCString( ctx, msg ); }-- return JS_UNDEFINED;--}----static JSValue OVCJS_error( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )--{-- if ( argc < 1 ) return JS_UNDEFINED;-- const char *msg = JS_ToCString( ctx, argv[0] );-- if ( msg ) { Warning( "[OV JS/client ERROR] %s\n", msg ); JS_FreeCString( ctx, msg ); }-- return JS_UNDEFINED;--}----static JSValue OVCJS_isServer( JSContext *ctx, JSValueConst, int, JSValueConst * )--{-- return JS_FALSE; // client realm--}----static JSValue OVCJS_getMode( JSContext *ctx, JSValueConst, int, JSValueConst * )--{-- return JS_NewString( ctx, g_OVClientJS.GetMode() );--}----static JSValue OVCJS_getMapName( JSContext *ctx, JSValueConst, int, JSValueConst * )--{-- return JS_NewString( ctx, engine ? engine->GetLevelName() : "" );--}++static void OVC_log( const char *m ) { Msg( "[OV JS/client] %s\n", m ); }++static void OVC_warn( const char *m ) { Warning( "[OV JS/client] %s\n", m ); }++static void OVC_error( const char *m ){ Warning( "[OV JS/client ERROR] %s\n", m ); }--static JSValue OVCJS_time( JSContext *ctx, JSValueConst, int, JSValueConst * )++static char *OVC_readFile( const char *path ){-- return JS_NewFloat64( ctx, gpGlobals ? gpGlobals->curtime : 0.0 );++ 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;}--static JSValue OVCJS_readFile( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )++static int OVC_fileExists( const char *path ){-- if ( argc < 1 ) return JS_NULL;-- const char *path = JS_ToCString( ctx, argv[0] );-- if ( !path ) return JS_NULL;---- if ( !OVCJS_IsSafeModPath( path ) )-- {-- Warning( "[OV JS/client] readFile refused unsafe path: %s\n", path );-- JS_FreeCString( ctx, path );-- return JS_NULL;-- }---- FileHandle_t file = filesystem->Open( path, "rb", "MOD" );-- if ( !file ) { JS_FreeCString( ctx, path ); return JS_NULL; }---- int size = filesystem->Size( file );-- if ( size < 0 || size > 8 * 1024 * 1024 )-- {-- filesystem->Close( file );-- JS_FreeCString( ctx, path );-- return JS_NULL;-- }---- char *buffer = new char[size + 1];-- filesystem->Read( buffer, size, file );-- filesystem->Close( file );-- buffer[size] = '\0';---- JSValue out = JS_NewStringLen( ctx, buffer, size );-- delete[] buffer;-- JS_FreeCString( ctx, path );-- return out;--}----static JSValue OVCJS_fileExists( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )--{-- if ( argc < 1 ) return JS_FALSE;-- const char *path = JS_ToCString( ctx, argv[0] );-- if ( !path ) return JS_FALSE;-- bool exists = OVCJS_IsSafeModPath( path )++ return OVC_IsSafeModPath( path )&& filesystem->FileExists( path, "MOD" )-- && !filesystem->IsDirectory( path, "MOD" );-- JS_FreeCString( ctx, path );-- return exists ? JS_TRUE : JS_FALSE;++ && !filesystem->IsDirectory( path, "MOD" ) ? 1 : 0;}--static JSValue OVCJS_listDir( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )++static char *OVC_listDir( const char *dir, const char *wildcard ){-- JSValue arr = JS_NewArray( ctx );-- if ( argc < 1 ) return arr;---- const char *dir = JS_ToCString( ctx, argv[0] );-- if ( !dir ) return arr;---- const char *wildcardArg = ( argc >= 2 && JS_IsString( argv[1] ) ) ? JS_ToCString( ctx, argv[1] ) : nullptr;-- const char *wildcard = ( wildcardArg && wildcardArg[0] ) ? wildcardArg : "*";---- if ( !OVCJS_IsSafeModPath( dir ) )-- {-- if ( wildcardArg ) JS_FreeCString( ctx, wildcardArg );-- JS_FreeCString( ctx, dir );-- return arr;-- }--++ if ( !OVC_IsSafeModPath( dir ) ) return NULL;char search[512];-- Q_snprintf( search, sizeof( search ), "%s/%s", dir, wildcard );++ Q_snprintf( search, sizeof( search ), "%s/%s", dir, ( wildcard && wildcard[0] ) ? wildcard : "*" );-- FileFindHandle_t findHandle;-- const char *name = filesystem->FindFirstEx( search, "MOD", &findHandle );-- uint32 index = 0;++ CUtlString out;++ FileFindHandle_t h;++ const char *name = filesystem->FindFirstEx( search, "MOD", &h );++ bool first = true;while ( name ){if ( Q_strcmp( name, "." ) != 0 && Q_strcmp( name, ".." ) != 0 )-- JS_SetPropertyUint32( ctx, arr, index++, JS_NewString( ctx, name ) );-- name = filesystem->FindNext( findHandle );++ {++ if ( !first ) out += "\n";++ out += name;++ first = false;++ }++ name = filesystem->FindNext( h );}-- filesystem->FindClose( findHandle );++ filesystem->FindClose( h );-- if ( wildcardArg ) JS_FreeCString( ctx, wildcardArg );-- JS_FreeCString( ctx, dir );-- return arr;++ 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;}--// client -> server: forward as the ov_net command the server JS dispatches.--static JSValue OVCJS_netSendToServer( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )++static int OVC_isServer( void ) { return 0; }++static const char *OVC_getMode( void ) { return ov_client_mode.GetString(); }++++static const char *OVC_getMapName( void ){-- if ( argc < 2 ) return JS_UNDEFINED;-- const char *name = JS_ToCString( ctx, argv[0] );-- const char *payload = JS_ToCString( ctx, argv[1] );-- if ( name && payload && engine )-- {-- char cmd[16384];-- Q_snprintf( cmd, sizeof( cmd ), "ov_net %s %s\n", name, payload );-- engine->ClientCmd_Unrestricted( cmd );-- }-- if ( name ) JS_FreeCString( ctx, name );-- if ( payload ) JS_FreeCString( ctx, payload );-- return JS_UNDEFINED;++ static char s_map[128];++ const char *lvl = engine ? engine->GetLevelName() : "";++ Q_strncpy( s_map, lvl ? lvl : "", sizeof( s_map ) );++ return s_map;}--// broadcast/serverCommand are server-only; provide inert client stubs so shared--// JS that references them doesn't throw on the client.--static JSValue OVCJS_noop( JSContext *ctx, JSValueConst, int, JSValueConst * )++static double OVC_getTime( void ) { return gpGlobals ? gpGlobals->curtime : 0.0; }++++static void OVC_netSendToServer( const char *name, const char *payloadB64 ){-- return JS_UNDEFINED;++ if ( !name || !payloadB64 || !engine ) return;++ char cmd[16384];++ Q_snprintf( cmd, sizeof( cmd ), "ov_net %s %s\n", name, payloadB64 );++ engine->ClientCmd_Unrestricted( cmd );}--// Register imperatively with JS_NewCFunction instead of a JS_CFUNC_DEF table.--// JS_CFUNC_DEF expands to C designated initializers (.u = { .func = ... }),--// which MSVC rejects before /std:c++20 (C7555/C7556); JS_NewCFunction is plain--// function calls that compile cleanly on both gcc and MSVC.--static void OVCJS_Bind( JSContext *ctx, JSValue ov, const char *name, JSCFunction *fn, int length )++static void OVC_netEmit( const char *, const char *, const char * ){-- JS_SetPropertyStr( ctx, ov, name, JS_NewCFunction( ctx, fn, name, length ) );++ // server->client emit is server-only; inert on the client.}--// This is the symbol ov_js_runtime.cpp calls; the client project compiles this--// file instead of the server ov_js_bindings.cpp.--void OVJS_RegisterNativeBindings( JSContext *ctx, COpenVibeJSRuntime * )++static OVJSHost g_ClientHost ={-- JSValue global = JS_GetGlobalObject( ctx );-- JSValue ov = JS_NewObject( ctx );---- OVCJS_Bind( ctx, ov, "log", OVCJS_log, 1 );-- OVCJS_Bind( ctx, ov, "warn", OVCJS_warn, 1 );-- OVCJS_Bind( ctx, ov, "error", OVCJS_error, 1 );-- OVCJS_Bind( ctx, ov, "isServer", OVCJS_isServer, 0 );-- OVCJS_Bind( ctx, ov, "getMode", OVCJS_getMode, 0 );-- OVCJS_Bind( ctx, ov, "getMapName", OVCJS_getMapName, 0 );-- OVCJS_Bind( ctx, ov, "time", OVCJS_time, 0 );-- OVCJS_Bind( ctx, ov, "readFile", OVCJS_readFile, 1 );-- OVCJS_Bind( ctx, ov, "fileExists", OVCJS_fileExists, 1 );-- OVCJS_Bind( ctx, ov, "listDir", OVCJS_listDir, 2 );-- OVCJS_Bind( ctx, ov, "netSendToServer", OVCJS_netSendToServer, 2 );-- OVCJS_Bind( ctx, ov, "broadcast", OVCJS_noop, 1 );-- OVCJS_Bind( ctx, ov, "serverCommand", OVCJS_noop, 1 );---- JS_SetPropertyStr( ctx, global, "OV", ov );-- JS_FreeValue( ctx, global );--}++ OVC_log, OVC_warn, OVC_error,++ OVC_readFile, OVC_fileExists, OVC_listDir,++ OVC_isServer, OVC_getMode, OVC_getMapName, OVC_getTime,++ OVC_netSendToServer, OVC_netEmit++};// ---------------------------------------------------------------------------// server -> client: OVNet usermessage -> OVNetReceive hook into client JS.// ---------------------------------------------------------------------------void __MsgFunc_OVNet( bf_read &msg ){-- if ( !OpenVibeClientJS_IsRunning() )-- return;--++ if ( !g_pClientCore ) return;char name[128];char payload[16384];msg.ReadString( name, sizeof( name ) );msg.ReadString( payload, sizeof( payload ) );---- JSContext *ctx = g_OVClientJS.Context();-- JSValue jsName = JS_NewString( ctx, name );-- JSValue jsPayload = JS_NewString( ctx, payload );-- JSValue jsPly = JS_NULL; // server->client has no "sending player"---- JSValueConst argv[] = { jsName, jsPayload, jsPly };-- g_OVClientJS.CallHookVoid( "OVNetReceive", 3, argv );---- JS_FreeValue( ctx, jsName );-- JS_FreeValue( ctx, jsPayload );++ // ply arg is null on the client (server->client has no sending player).++ ovjs_fire_hook_s( g_pClientCore, "OVNetReceive", name, payload, NULL );}// ---------------------------------------------------------------------------@@@ -262,20 +144,17 @@ void __MsgFunc_OVNet( bf_read &msg )// ---------------------------------------------------------------------------void OpenVibeJS_Client_Init(){-- if ( g_OVClientJSStarted )-- return;-- g_OVClientJSStarted = true;--++ if ( g_pClientCore ) return;if ( !ov_client_js_enabled.GetBool() ){Msg( "[OV JS/client] disabled by ov_client_js_enabled=0\n" );return;}---- if ( g_OVClientJS.Init( false /*client realm*/, ov_client_mode.GetString() ) )++ 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() );-- g_OVClientJS.CallHookVoid( "Initialize" );++ ovjs_fire_hook( g_pClientCore, "Initialize" );}else{@@@ -285,27 +164,28 @@ void OpenVibeJS_Client_Init()void OpenVibeJS_Client_Shutdown(){-- if ( OpenVibeClientJS_IsRunning() )-- g_OVClientJS.CallHookVoid( "Shutdown" );-- g_OVClientJS.Shutdown();-- g_OVClientJSStarted = false;++ if ( g_pClientCore )++ {++ ovjs_fire_hook( g_pClientCore, "Shutdown" );++ ovjs_destroy( g_pClientCore );++ g_pClientCore = NULL;++ }}void OpenVibeJS_Client_Think(){-- if ( OpenVibeClientJS_IsRunning() )-- g_OVClientJS.CallHookVoid( "Think" );++ if ( g_pClientCore )++ ovjs_fire_hook( g_pClientCore, "Think" );}// -----------------------------------------------------------------------------// Client concommands (mirror the server ones for parity/debugging).++// Client concommands.// ---------------------------------------------------------------------------static void OV_ClientJSStatus_f(){-- Msg( "[OV JS/client] enabled=%d started=%d running=%d mode=%s\n",++ Msg( "[OV JS/client] enabled=%d running=%d mode=%s\n",ov_client_js_enabled.GetBool() ? 1 : 0,-- g_OVClientJSStarted ? 1 : 0,-- OpenVibeClientJS_IsRunning() ? 1 : 0,++ g_pClientCore ? 1 : 0,ov_client_mode.GetString() );}static ConCommand ov_client_js_status( "ov_client_js_status", OV_ClientJSStatus_f,@@@ -320,33 +200,25 @@ static void OV_ClientJSReload_f()static ConCommand ov_client_js_reload( "ov_client_js_reload", OV_ClientJSReload_f,"Reload the OpenVibe client JavaScript runtime.", FCVAR_CLIENTDLL );--// Drives the client JS runtime: hooks the OVNet usermessage at load, starts the--// runtime once the level/systems are up, and pumps Think() each frame (for JS--// timers). Self-contained so no external init-call patching is needed.++// Drives the runtime: hook OVNet at load, start on level enter, Think per frame.class COpenVibeClientJSSystem : public CAutoGameSystemPerFrame{public:COpenVibeClientJSSystem() : CAutoGameSystemPerFrame( "COpenVibeClientJSSystem" ) {}--bool Init() OVERRIDE{usermessages->HookMessage( "OVNet", __MsgFunc_OVNet );return true;}--void LevelInitPostEntity() OVERRIDE{-- // (Re)start the runtime when a level is entered so <mode>/client.js and-- // client addons load fresh for the connected server.OpenVibeJS_Client_Shutdown();OpenVibeJS_Client_Init();}--void LevelShutdownPostEntity() OVERRIDE{OpenVibeJS_Client_Shutdown();}--void Update( float frametime ) OVERRIDE{OpenVibeJS_Client_Think();nnew file mode 100644@@@ -0,0 +1,324 @@++/* ovjs_core.c — host-agnostic OpenVibe JavaScript core. Compiled AS C so it can++ * include QuickJS's C headers cleanly (see ovjs_core.h for the why). */++#include "ovjs_core.h"++#include "third_party/quickjs/quickjs.h"++++#include <stdlib.h>++#include <string.h>++++struct OVJSCore++{++ JSRuntime *rt;++ JSContext *ctx;++ const OVJSHost *host;++ int isServer;++ char mode[64];++};++++/* The active core is stored as the context opaque so bindings can reach host. */++static OVJSCore *OVJS_FromCtx(JSContext *ctx)++{++ return (OVJSCore *)JS_GetContextOpaque(ctx);++}++++/* ------------------------------------------------------------------ */++/* OV.* native bindings — thin shims over the host callbacks. */++/* ------------------------------------------------------------------ */++static JSValue ov_log(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ if (argc >= 1 && c && c->host && c->host->log) {++ const char *s = JS_ToCString(ctx, argv[0]);++ if (s) { c->host->log(s); JS_FreeCString(ctx, s); }++ }++ return JS_UNDEFINED;++}++static JSValue ov_warn(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ if (argc >= 1 && c && c->host && c->host->warn) {++ const char *s = JS_ToCString(ctx, argv[0]);++ if (s) { c->host->warn(s); JS_FreeCString(ctx, s); }++ }++ return JS_UNDEFINED;++}++static JSValue ov_error(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ if (argc >= 1 && c && c->host && c->host->error) {++ const char *s = JS_ToCString(ctx, argv[0]);++ if (s) { c->host->error(s); JS_FreeCString(ctx, s); }++ }++ return JS_UNDEFINED;++}++static JSValue ov_isServer(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ return JS_NewBool(ctx, c ? c->isServer : 0);++}++static JSValue ov_getMode(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ return JS_NewString(ctx, c ? c->mode : "");++}++static JSValue ov_getMapName(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ const char *m = (c && c->host && c->host->getMapName) ? c->host->getMapName() : "";++ return JS_NewString(ctx, m ? m : "");++}++static JSValue ov_time(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ double v = (c && c->host && c->host->getTime) ? c->host->getTime() : 0.0;++ return JS_NewFloat64(ctx, v);++}++static JSValue ov_readFile(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ if (argc < 1 || !c || !c->host || !c->host->readFile) return JS_NULL;++ const char *path = JS_ToCString(ctx, argv[0]);++ if (!path) return JS_NULL;++ char *buf = c->host->readFile(path);++ JS_FreeCString(ctx, path);++ if (!buf) return JS_NULL;++ JSValue out = JS_NewString(ctx, buf);++ free(buf);++ return out;++}++static JSValue ov_fileExists(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ if (argc < 1 || !c || !c->host || !c->host->fileExists) return JS_FALSE;++ const char *path = JS_ToCString(ctx, argv[0]);++ if (!path) return JS_FALSE;++ int ok = c->host->fileExists(path);++ JS_FreeCString(ctx, path);++ return JS_NewBool(ctx, ok);++}++static JSValue ov_listDir(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ JSValue arr = JS_NewArray(ctx);++ if (argc < 1 || !c || !c->host || !c->host->listDir) return arr;++ const char *dir = JS_ToCString(ctx, argv[0]);++ const char *wc = (argc >= 2) ? JS_ToCString(ctx, argv[1]) : NULL;++ if (dir) {++ char *joined = c->host->listDir(dir, (wc && wc[0]) ? wc : "*");++ if (joined) {++ uint32_t idx = 0;++ char *p = joined, *start = joined;++ for (;; ++p) {++ if (*p == '\n' || *p == '\0') {++ if (p > start) {++ JS_SetPropertyUint32(ctx, arr, idx++,++ JS_NewStringLen(ctx, start, (size_t)(p - start)));++ }++ if (*p == '\0') break;++ start = p + 1;++ }++ }++ free(joined);++ }++ }++ if (dir) JS_FreeCString(ctx, dir);++ if (wc) JS_FreeCString(ctx, wc);++ return arr;++}++static JSValue ov_broadcast(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ /* Server-only chat broadcast is handled by the server bindings; on the++ * client this is an inert stub so shared JS doesn't throw. */++ return JS_UNDEFINED;++}++static JSValue ov_serverCommand(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ return JS_UNDEFINED;++}++static JSValue ov_netSendToServer(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ if (argc < 2 || !c || !c->host || !c->host->netSendToServer) return JS_UNDEFINED;++ const char *name = JS_ToCString(ctx, argv[0]);++ const char *pl = JS_ToCString(ctx, argv[1]);++ if (name && pl) c->host->netSendToServer(name, pl);++ if (name) JS_FreeCString(ctx, name);++ if (pl) JS_FreeCString(ctx, pl);++ return JS_UNDEFINED;++}++static JSValue ov_netEmit(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)++{++ OVJSCore *c = OVJS_FromCtx(ctx);++ if (argc < 3 || !c || !c->host || !c->host->netEmit) return JS_UNDEFINED;++ const char *ids = JS_ToCString(ctx, argv[0]);++ const char *name = JS_ToCString(ctx, argv[1]);++ const char *pl = JS_ToCString(ctx, argv[2]);++ if (ids && name && pl) c->host->netEmit(ids, name, pl);++ if (ids) JS_FreeCString(ctx, ids);++ if (name) JS_FreeCString(ctx, name);++ if (pl) JS_FreeCString(ctx, pl);++ return JS_UNDEFINED;++}++++static const JSCFunctionListEntry ov_funcs[] = {++ JS_CFUNC_DEF("log", 1, ov_log),++ JS_CFUNC_DEF("warn", 1, ov_warn),++ JS_CFUNC_DEF("error", 1, ov_error),++ JS_CFUNC_DEF("isServer", 0, ov_isServer),++ JS_CFUNC_DEF("getMode", 0, ov_getMode),++ JS_CFUNC_DEF("getMapName", 0, ov_getMapName),++ JS_CFUNC_DEF("time", 0, ov_time),++ JS_CFUNC_DEF("readFile", 1, ov_readFile),++ JS_CFUNC_DEF("fileExists", 1, ov_fileExists),++ JS_CFUNC_DEF("listDir", 2, ov_listDir),++ JS_CFUNC_DEF("broadcast", 1, ov_broadcast),++ JS_CFUNC_DEF("serverCommand", 1, ov_serverCommand),++ JS_CFUNC_DEF("netSendToServer", 2, ov_netSendToServer),++ JS_CFUNC_DEF("netEmit", 3, ov_netEmit),++};++++static void OVJS_ReportException(OVJSCore *c, const char *where)++{++ JSValue exc = JS_GetException(c->ctx);++ const char *msg = JS_ToCString(c->ctx, exc);++ if (c->host && c->host->error) {++ char buf[1024];++ snprintf(buf, sizeof(buf), "[%s] %s", where ? where : "eval", msg ? msg : "unknown");++ c->host->error(buf);++ }++ if (msg) JS_FreeCString(c->ctx, msg);++ JS_FreeValue(c->ctx, exc);++}++++int ovjs_eval(OVJSCore *c, const char *code, const char *filename)++{++ if (!c || !c->ctx || !code) return 0;++ JSValue r = JS_Eval(c->ctx, code, strlen(code),++ filename ? filename : "<openvibe>", JS_EVAL_TYPE_GLOBAL);++ if (JS_IsException(r)) {++ OVJS_ReportException(c, filename);++ JS_FreeValue(c->ctx, r);++ return 0;++ }++ JS_FreeValue(c->ctx, r);++ return 1;++}++++static int OVJS_LoadFile(OVJSCore *c, const char *path)++{++ if (!c->host || !c->host->readFile) return 0;++ char *code = c->host->readFile(path);++ if (!code) {++ if (c->host->warn) {++ char buf[512];++ snprintf(buf, sizeof(buf), "core: could not read %s", path);++ c->host->warn(buf);++ }++ return 0;++ }++ int ok = ovjs_eval(c, code, path);++ free(code);++ return ok;++}++++static int OVJS_LoadCoreFiles(OVJSCore *c)++{++ if (!OVJS_LoadFile(c, "js/core/hook.js")) return 0;++ if (!OVJS_LoadFile(c, "js/core/gamemode.js")) return 0;++ OVJS_LoadFile(c, "js/bridge.js"); /* bootstraps module/net/addon */++ OVJS_LoadFile(c, "js/core/command.js");++ OVJS_LoadFile(c, "js/core/timer.js");++ return 1;++}++++static void OVJS_LoadGamemode(OVJSCore *c)++{++ char path[256];++ OVJS_LoadFile(c, "js/gamemodes/base/server.js"); /* base defines shared GM */++ snprintf(path, sizeof(path), "js/gamemodes/%s/%s.js",++ c->mode, c->isServer ? "server" : "client");++ OVJS_LoadFile(c, path);++}++++static void OVJS_RegisterBindings(OVJSCore *c)++{++ JSValue global = JS_GetGlobalObject(c->ctx);++ JSValue ov = JS_NewObject(c->ctx);++ JS_SetPropertyFunctionList(c->ctx, ov, ov_funcs,++ (int)(sizeof(ov_funcs) / sizeof(ov_funcs[0])));++ JS_SetPropertyStr(c->ctx, global, "OV", ov);++ JS_FreeValue(c->ctx, global);++}++++OVJSCore *ovjs_create(const OVJSHost *host, int isServerRealm, const char *mode)++{++ OVJSCore *c = (OVJSCore *)calloc(1, sizeof(OVJSCore));++ if (!c) return NULL;++ c->host = host;++ c->isServer = isServerRealm ? 1 : 0;++ if (mode && mode[0]) { strncpy(c->mode, mode, sizeof(c->mode) - 1); }++ else { strncpy(c->mode, "hub", sizeof(c->mode) - 1); }++++ c->rt = JS_NewRuntime();++ if (!c->rt) { free(c); return NULL; }++ JS_SetMemoryLimit(c->rt, 32 * 1024 * 1024);++ JS_SetMaxStackSize(c->rt, 1024 * 1024);++++ c->ctx = JS_NewContext(c->rt);++ if (!c->ctx) { JS_FreeRuntime(c->rt); free(c); return NULL; }++ JS_SetContextOpaque(c->ctx, c);++++ OVJS_RegisterBindings(c);++ if (!OVJS_LoadCoreFiles(c)) { ovjs_destroy(c); return NULL; }++ OVJS_LoadGamemode(c);++ return c;++}++++void ovjs_destroy(OVJSCore *c)++{++ if (!c) return;++ if (c->ctx) JS_FreeContext(c->ctx);++ if (c->rt) JS_FreeRuntime(c->rt);++ free(c);++}++++/* Fire gamemode.call(hook, args...) — mirrors the server bridge's hook dispatch. */++static void OVJS_FireHookArgv(OVJSCore *c, const char *hook, int argc, JSValueConst *argv)++{++ JSValue global = JS_GetGlobalObject(c->ctx);++ JSValue gamemode = JS_GetPropertyStr(c->ctx, global, "gamemode");++ JSValue call = JS_GetPropertyStr(c->ctx, gamemode, "call");++ if (JS_IsFunction(c->ctx, call)) {++ int n = argc + 1;++ JSValue *args = (JSValue *)malloc(sizeof(JSValue) * (n > 0 ? n : 1));++ args[0] = JS_NewString(c->ctx, hook);++ for (int i = 0; i < argc; ++i) args[i + 1] = argv[i];++ JSValue r = JS_Call(c->ctx, call, gamemode, n, args);++ if (JS_IsException(r)) OVJS_ReportException(c, hook);++ JS_FreeValue(c->ctx, r);++ JS_FreeValue(c->ctx, args[0]);++ free(args);++ }++ JS_FreeValue(c->ctx, call);++ JS_FreeValue(c->ctx, gamemode);++ JS_FreeValue(c->ctx, global);++}++++void ovjs_fire_hook(OVJSCore *c, const char *hook)++{++ if (!c || !c->ctx || !hook) return;++ OVJS_FireHookArgv(c, hook, 0, NULL);++}++++void ovjs_fire_hook_s(OVJSCore *c, const char *hook,++ const char *a, const char *b, const char *d)++{++ if (!c || !c->ctx || !hook) return;++ JSValue argv[3];++ int argc = 0;++ if (a) { argv[argc++] = JS_NewString(c->ctx, a); }++ if (a && b) { argv[argc++] = JS_NewString(c->ctx, b); }++ if (a && b && d) { argv[argc++] = JS_NewString(c->ctx, d); }++ OVJS_FireHookArgv(c, hook, argc, argv);++ for (int i = 0; i < argc; ++i) JS_FreeValue(c->ctx, argv[i]);++}nnew file mode 100644@@@ -0,0 +1,64 @@++/* ovjs_core.h — host-agnostic OpenVibe JavaScript core (C ABI).++ *++ * This is the boundary that lets the client DLL embed QuickJS on Windows.++ * The core (ovjs_core.c) is compiled AS C, so it may include quickjs.h — whose++ * C99 compound literals / designated initializers are valid in C but rejected by++ * MSVC cl.exe in C++ mode. The Source-SDK glue (openvibe_js_client.cpp) is C++++ * and includes ONLY this pure-C-ABI header, never quickjs.h, so cl.exe never++ * parses QuickJS. The core reaches the engine (logging, files, networking)++ * exclusively through the host callbacks below.++ */++#ifndef OVJS_CORE_H++#define OVJS_CORE_H++++#ifdef __cplusplus++extern "C" {++#endif++++typedef struct OVJSCore OVJSCore;++++/* Callbacks the JS core invokes; implemented by the host (Source SDK glue).++ * String returns marked "(host-owned static)" must remain valid until the next++ * call. readFile/listDir return malloc()'d buffers the CORE frees with free(). */++typedef struct OVJSHost++{++ void (*log)(const char *msg);++ void (*warn)(const char *msg);++ void (*error)(const char *msg);++++ /* Returns malloc'd NUL-terminated file contents (core frees), or NULL. */++ char *(*readFile)(const char *path);++ int (*fileExists)(const char *path);++ /* Returns malloc'd newline-joined entry names (core frees), or NULL. */++ char *(*listDir)(const char *dir, const char *wildcard);++++ int (*isServer)(void);++ const char *(*getMode)(void); /* host-owned static */++ const char *(*getMapName)(void); /* host-owned static */++ double (*getTime)(void);++++ /* client -> server */++ void (*netSendToServer)(const char *name, const char *payloadB64);++ /* server -> client (idsCsv: comma-separated userIds, "-1" = broadcast) */++ void (*netEmit)(const char *idsCsv, const char *name, const char *payloadB64);++} OVJSHost;++++/* Create a runtime, register the OV.* bindings, and load the core JS files plus++ * js/gamemodes/<mode>/(server|client).js. Returns NULL on failure. */++OVJSCore *ovjs_create(const OVJSHost *host, int isServerRealm, const char *mode);++void ovjs_destroy(OVJSCore *core);++++/* Evaluate a script. Returns 1 on success, 0 on error (reported via host->error). */++int ovjs_eval(OVJSCore *core, const char *code, const char *filename);++++/* Fire a gamemode/hook by name. The _s variant passes up to three string args++ * (pass NULL to stop early); used for events like OVNetReceive(name,payload). */++void ovjs_fire_hook(OVJSCore *core, const char *hook);++void ovjs_fire_hook_s(OVJSCore *core, const char *hook,++ const char *a, const char *b, const char *c);++++#ifdef __cplusplus++}++#endif++++#endif /* OVJS_CORE_H */@@@ -54,6 +54,10 @@ 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/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/server/hl2mp/openvibe_js_server.h" \"$SDK/src/game/server/hl2mp/openvibe_js_server.h"@@@ -79,8 +83,9 @@ perl -0pi -e 's/^.*hl2mp\\vgui_openvibe_menu\.cpp.*\n//mg;s/^.*hl2mp\\openvibe_js_client\.cpp.*\n//mg;s/^.*openvibe\\ov_js_runtime\.cpp.*\n//mg;++ s/^.*openvibe\\ovjs_core\.c".*\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\\ov_js_runtime.cpp"\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\\ovjs_core.c" [\$LINUXALL]\n\t\t\t\$Lib\t"..\\shared\\openvibe\\third_party\\quickjs\\build\\libquickjs_openvibe"\n/s;' "$CLIENT_VPC"echo "[openvibe-sdk] patched client_hl2mp.vpc"@@@ -282,6 +282,25 @@ foreach ($src in $sources) {$objects += $obj}++# Also compile the OpenVibe host-agnostic JS core (ovjs_core.c) with the SAME++# clang-cl toolchain and pack it into libquickjs_openvibe.lib. ovjs_core.c is C++# and includes quickjs.h, so it must NOT be compiled by cl.exe in C++ mode — this++# is exactly why the client glue talks to it through the plain C ABI in++# ovjs_core.h. Compiling it here means the client project just links this lib.++$SdkOpenvibe = Join-Path $Sdk 'src/game/shared/openvibe'++$OvjsCoreSrc = Join-Path $SdkOpenvibe 'ovjs_core.c'++if (Test-Path $OvjsCoreSrc) {++ $ovjsObj = Join-Path $Out 'ovjs_core.obj'++ $CoreArgs = $Common + @("/I$SdkOpenvibe")++ Say "$([IO.Path]::GetFileName($Compiler)) ovjs_core.c -> $ovjsObj"++ & $Compiler @CoreArgs /c "$OvjsCoreSrc" "/Fo$ovjsObj"++ if ($LASTEXITCODE -ne 0) { throw "$Compiler failed for ovjs_core.c" }++ if (!(Test-Path $ovjsObj)) { throw "$Compiler reported success but did not produce $ovjsObj" }++ $objects += $ovjsObj++} else {++ Say "ovjs_core.c not present at $OvjsCoreSrc; skipping (client JS core will be unresolved)"++}++$outLib = Join-Path $Out 'libquickjs_openvibe.lib'Say "lib -> $outLib"& lib.exe /nologo "/OUT:$outLib" $objects
@@ -1,260 +1,142 @@
@@ -1,260 +1,142 @@
#include "cbase.h"
#include "cbase.h"
#include "openvibe_js_client.h"
#include "openvibe_js_client.h"
-// The client always uses the real QuickJS runtime (the Windows *server* stub
-// guard keys on GAME_DLL, which is not defined in the client build).
+// 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"
#include "filesystem.h"
-#include "openvibe/ov_js_runtime.h"
-#include "openvibe/ov_js_bindings.h"
#include "usermessages.h"
#include "usermessages.h"
-#include <ctype.h>
+#include <stdlib.h>
+#include <string.h>
#include "tier0/memdbgon.h"
#include "tier0/memdbgon.h"
-// ---------------------------------------------------------------------------
-// Client JS runtime instance + mode.
-// ---------------------------------------------------------------------------
-static COpenVibeJSRuntime g_OVClientJS;
-static bool g_OVClientJSStarted = false;
+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 JavaScript runtime." );
-
-// Which gamemode's client.js to load. The server pushes its mode to the client
-// (ov_client_mode <mode>; then the runtime reloads), defaulting to hub.
static ConVar ov_client_mode(
static ConVar ov_client_mode(
"ov_client_mode", "hub", FCVAR_CLIENTDLL,
"ov_client_mode", "hub", FCVAR_CLIENTDLL,
"OpenVibe client gamemode realm: hub, prophunt, deathrun, fortwars, traitortown, sandbox." );
"OpenVibe client gamemode realm: hub, prophunt, deathrun, fortwars, traitortown, sandbox." );
-static bool OpenVibeClientJS_IsRunning()
-{
- return ov_client_js_enabled.GetBool() && g_OVClientJS.Context() != nullptr;
-}
-
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-// Client OV.* native bindings. Client-safe subset (no server player list /
-// ServerCommand). File I/O + logging + net + realm info, matching the server
-// bridge so the shared core JS (require/net/addon/timer/hook) runs unchanged.
+// Host callbacks (C ABI). Source SDK lives entirely on this side.
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-static bool OVCJS_IsSafeModPath( const char *pszPath )
+static bool OVC_IsSafeModPath( const char *p )
{
{
- if ( !pszPath || !pszPath[0] ) return false;
- if ( pszPath[0] == '/' || pszPath[0] == '\\' ) return false;
- if ( Q_strstr( pszPath, ".." ) ) return false;
- if ( Q_strstr( pszPath, ":" ) ) return false;
+ 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;
return true;
}
}
-static JSValue OVCJS_log( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )
-{
- if ( argc < 1 ) return JS_UNDEFINED;
- const char *msg = JS_ToCString( ctx, argv[0] );
- if ( msg ) { Msg( "[OV JS/client] %s\n", msg ); JS_FreeCString( ctx, msg ); }
- return JS_UNDEFINED;
-}
-
-static JSValue OVCJS_warn( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )
-{
- if ( argc < 1 ) return JS_UNDEFINED;
- const char *msg = JS_ToCString( ctx, argv[0] );
- if ( msg ) { Warning( "[OV JS/client] %s\n", msg ); JS_FreeCString( ctx, msg ); }
- return JS_UNDEFINED;
-}
-
-static JSValue OVCJS_error( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )
-{
- if ( argc < 1 ) return JS_UNDEFINED;
- const char *msg = JS_ToCString( ctx, argv[0] );
- if ( msg ) { Warning( "[OV JS/client ERROR] %s\n", msg ); JS_FreeCString( ctx, msg ); }
- return JS_UNDEFINED;
-}
-
-static JSValue OVCJS_isServer( JSContext *ctx, JSValueConst, int, JSValueConst * )
-{
- return JS_FALSE; // client realm
-}
-
-static JSValue OVCJS_getMode( JSContext *ctx, JSValueConst, int, JSValueConst * )
-{
- return JS_NewString( ctx, g_OVClientJS.GetMode() );
-}
-
-static JSValue OVCJS_getMapName( JSContext *ctx, JSValueConst, int, JSValueConst * )
-{
- return JS_NewString( ctx, engine ? engine->GetLevelName() : "" );
-}
+static void OVC_log( const char *m ) { Msg( "[OV JS/client] %s\n", m ); }
+static void OVC_warn( const char *m ) { Warning( "[OV JS/client] %s\n", m ); }
+static void OVC_error( const char *m ){ Warning( "[OV JS/client ERROR] %s\n", m ); }
-static JSValue OVCJS_time( JSContext *ctx, JSValueConst, int, JSValueConst * )
+static char *OVC_readFile( const char *path )
{
{
- return JS_NewFloat64( ctx, gpGlobals ? gpGlobals->curtime : 0.0 );
+ 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;
}
}
-static JSValue OVCJS_readFile( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )
+static int OVC_fileExists( const char *path )
{
{
- if ( argc < 1 ) return JS_NULL;
- const char *path = JS_ToCString( ctx, argv[0] );
- if ( !path ) return JS_NULL;
-
- if ( !OVCJS_IsSafeModPath( path ) )
- {
- Warning( "[OV JS/client] readFile refused unsafe path: %s\n", path );
- JS_FreeCString( ctx, path );
- return JS_NULL;
- }
-
- FileHandle_t file = filesystem->Open( path, "rb", "MOD" );
- if ( !file ) { JS_FreeCString( ctx, path ); return JS_NULL; }
-
- int size = filesystem->Size( file );
- if ( size < 0 || size > 8 * 1024 * 1024 )
- {
- filesystem->Close( file );
- JS_FreeCString( ctx, path );
- return JS_NULL;
- }
-
- char *buffer = new char[size + 1];
- filesystem->Read( buffer, size, file );
- filesystem->Close( file );
- buffer[size] = '\0';
-
- JSValue out = JS_NewStringLen( ctx, buffer, size );
- delete[] buffer;
- JS_FreeCString( ctx, path );
- return out;
-}
-
-static JSValue OVCJS_fileExists( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )
-{
- if ( argc < 1 ) return JS_FALSE;
- const char *path = JS_ToCString( ctx, argv[0] );
- if ( !path ) return JS_FALSE;
- bool exists = OVCJS_IsSafeModPath( path )
+ return OVC_IsSafeModPath( path )
&& filesystem->FileExists( path, "MOD" )
&& filesystem->FileExists( path, "MOD" )
- && !filesystem->IsDirectory( path, "MOD" );
- JS_FreeCString( ctx, path );
- return exists ? JS_TRUE : JS_FALSE;
+ && !filesystem->IsDirectory( path, "MOD" ) ? 1 : 0;
}
}
-static JSValue OVCJS_listDir( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )
+static char *OVC_listDir( const char *dir, const char *wildcard )
{
{
- JSValue arr = JS_NewArray( ctx );
- if ( argc < 1 ) return arr;
-
- const char *dir = JS_ToCString( ctx, argv[0] );
- if ( !dir ) return arr;
-
- const char *wildcardArg = ( argc >= 2 && JS_IsString( argv[1] ) ) ? JS_ToCString( ctx, argv[1] ) : nullptr;
- const char *wildcard = ( wildcardArg && wildcardArg[0] ) ? wildcardArg : "*";
-
- if ( !OVCJS_IsSafeModPath( dir ) )
- {
- if ( wildcardArg ) JS_FreeCString( ctx, wildcardArg );
- JS_FreeCString( ctx, dir );
- return arr;
- }
-
+ if ( !OVC_IsSafeModPath( dir ) ) return NULL;
char search[512];
char search[512];
- Q_snprintf( search, sizeof( search ), "%s/%s", dir, wildcard );
+ Q_snprintf( search, sizeof( search ), "%s/%s", dir, ( wildcard && wildcard[0] ) ? wildcard : "*" );
- FileFindHandle_t findHandle;
- const char *name = filesystem->FindFirstEx( search, "MOD", &findHandle );
- uint32 index = 0;
+ CUtlString out;
+ FileFindHandle_t h;
+ const char *name = filesystem->FindFirstEx( search, "MOD", &h );
+ bool first = true;
while ( name )
while ( name )
{
{
if ( Q_strcmp( name, "." ) != 0 && Q_strcmp( name, ".." ) != 0 )
if ( Q_strcmp( name, "." ) != 0 && Q_strcmp( name, ".." ) != 0 )
- JS_SetPropertyUint32( ctx, arr, index++, JS_NewString( ctx, name ) );
- name = filesystem->FindNext( findHandle );
+ {
+ if ( !first ) out += "\n";
+ out += name;
+ first = false;
+ }
+ name = filesystem->FindNext( h );
}
}
- filesystem->FindClose( findHandle );
+ filesystem->FindClose( h );
- if ( wildcardArg ) JS_FreeCString( ctx, wildcardArg );
- JS_FreeCString( ctx, dir );
- return arr;
+ 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;
}
}
-// client -> server: forward as the ov_net command the server JS dispatches.
-static JSValue OVCJS_netSendToServer( JSContext *ctx, JSValueConst, int argc, JSValueConst *argv )
+static int OVC_isServer( void ) { return 0; }
+static const char *OVC_getMode( void ) { return ov_client_mode.GetString(); }
+
+static const char *OVC_getMapName( void )
{
{
- if ( argc < 2 ) return JS_UNDEFINED;
- const char *name = JS_ToCString( ctx, argv[0] );
- const char *payload = JS_ToCString( ctx, argv[1] );
- if ( name && payload && engine )
- {
- char cmd[16384];
- Q_snprintf( cmd, sizeof( cmd ), "ov_net %s %s\n", name, payload );
- engine->ClientCmd_Unrestricted( cmd );
- }
- if ( name ) JS_FreeCString( ctx, name );
- if ( payload ) JS_FreeCString( ctx, payload );
- return JS_UNDEFINED;
+ static char s_map[128];
+ const char *lvl = engine ? engine->GetLevelName() : "";
+ Q_strncpy( s_map, lvl ? lvl : "", sizeof( s_map ) );
+ return s_map;
}
}
-// broadcast/serverCommand are server-only; provide inert client stubs so shared
-// JS that references them doesn't throw on the client.
-static JSValue OVCJS_noop( JSContext *ctx, JSValueConst, int, JSValueConst * )
+static double OVC_getTime( void ) { return gpGlobals ? gpGlobals->curtime : 0.0; }
+
+static void OVC_netSendToServer( const char *name, const char *payloadB64 )
{
{
- return JS_UNDEFINED;
+ if ( !name || !payloadB64 || !engine ) return;
+ char cmd[16384];
+ Q_snprintf( cmd, sizeof( cmd ), "ov_net %s %s\n", name, payloadB64 );
+ engine->ClientCmd_Unrestricted( cmd );
}
}
-// Register imperatively with JS_NewCFunction instead of a JS_CFUNC_DEF table.
-// JS_CFUNC_DEF expands to C designated initializers (.u = { .func = ... }),
-// which MSVC rejects before /std:c++20 (C7555/C7556); JS_NewCFunction is plain
-// function calls that compile cleanly on both gcc and MSVC.
-static void OVCJS_Bind( JSContext *ctx, JSValue ov, const char *name, JSCFunction *fn, int length )
+static void OVC_netEmit( const char *, const char *, const char * )
{
{
- JS_SetPropertyStr( ctx, ov, name, JS_NewCFunction( ctx, fn, name, length ) );
+ // server->client emit is server-only; inert on the client.
}
}
-// This is the symbol ov_js_runtime.cpp calls; the client project compiles this
-// file instead of the server ov_js_bindings.cpp.
-void OVJS_RegisterNativeBindings( JSContext *ctx, COpenVibeJSRuntime * )
+static OVJSHost g_ClientHost =
{
{
- JSValue global = JS_GetGlobalObject( ctx );
- JSValue ov = JS_NewObject( ctx );
-
- OVCJS_Bind( ctx, ov, "log", OVCJS_log, 1 );
- OVCJS_Bind( ctx, ov, "warn", OVCJS_warn, 1 );
- OVCJS_Bind( ctx, ov, "error", OVCJS_error, 1 );
- OVCJS_Bind( ctx, ov, "isServer", OVCJS_isServer, 0 );
- OVCJS_Bind( ctx, ov, "getMode", OVCJS_getMode, 0 );
- OVCJS_Bind( ctx, ov, "getMapName", OVCJS_getMapName, 0 );
- OVCJS_Bind( ctx, ov, "time", OVCJS_time, 0 );
- OVCJS_Bind( ctx, ov, "readFile", OVCJS_readFile, 1 );
- OVCJS_Bind( ctx, ov, "fileExists", OVCJS_fileExists, 1 );
- OVCJS_Bind( ctx, ov, "listDir", OVCJS_listDir, 2 );
- OVCJS_Bind( ctx, ov, "netSendToServer", OVCJS_netSendToServer, 2 );
- OVCJS_Bind( ctx, ov, "broadcast", OVCJS_noop, 1 );
- OVCJS_Bind( ctx, ov, "serverCommand", OVCJS_noop, 1 );
-
- JS_SetPropertyStr( ctx, global, "OV", ov );
- JS_FreeValue( ctx, global );
-}
+ OVC_log, OVC_warn, OVC_error,
+ OVC_readFile, OVC_fileExists, OVC_listDir,
+ OVC_isServer, OVC_getMode, OVC_getMapName, OVC_getTime,
+ OVC_netSendToServer, OVC_netEmit
+};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// server -> client: OVNet usermessage -> OVNetReceive hook into client JS.
// server -> client: OVNet usermessage -> OVNetReceive hook into client JS.
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
void __MsgFunc_OVNet( bf_read &msg )
void __MsgFunc_OVNet( bf_read &msg )
{
{
- if ( !OpenVibeClientJS_IsRunning() )
- return;
-
+ if ( !g_pClientCore ) return;
char name[128];
char name[128];
char payload[16384];
char payload[16384];
msg.ReadString( name, sizeof( name ) );
msg.ReadString( name, sizeof( name ) );
msg.ReadString( payload, sizeof( payload ) );
msg.ReadString( payload, sizeof( payload ) );
-
- JSContext *ctx = g_OVClientJS.Context();
- JSValue jsName = JS_NewString( ctx, name );
- JSValue jsPayload = JS_NewString( ctx, payload );
- JSValue jsPly = JS_NULL; // server->client has no "sending player"
-
- JSValueConst argv[] = { jsName, jsPayload, jsPly };
- g_OVClientJS.CallHookVoid( "OVNetReceive", 3, argv );
-
- JS_FreeValue( ctx, jsName );
- JS_FreeValue( ctx, jsPayload );
+ // ply arg is null on the client (server->client has no sending player).
+ ovjs_fire_hook_s( g_pClientCore, "OVNetReceive", name, payload, NULL );
}
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
@@ -262,20 +144,17 @@ void __MsgFunc_OVNet( bf_read &msg )
@@ -262,20 +144,17 @@ void __MsgFunc_OVNet( bf_read &msg )
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
void OpenVibeJS_Client_Init()
void OpenVibeJS_Client_Init()
{
{
- if ( g_OVClientJSStarted )
- return;
- g_OVClientJSStarted = true;
-
+ if ( g_pClientCore ) return;
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] disabled by ov_client_js_enabled=0\n" );
return;
return;
}
}
-
- if ( g_OVClientJS.Init( false /*client realm*/, ov_client_mode.GetString() ) )
+ 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() );
Msg( "[OV JS/client] client runtime initialized for mode '%s'\n", ov_client_mode.GetString() );
- g_OVClientJS.CallHookVoid( "Initialize" );
+ ovjs_fire_hook( g_pClientCore, "Initialize" );
}
}
else
else
{
{
@@ -285,27 +164,28 @@ void OpenVibeJS_Client_Init()
@@ -285,27 +164,28 @@ void OpenVibeJS_Client_Init()
void OpenVibeJS_Client_Shutdown()
void OpenVibeJS_Client_Shutdown()
{
{
- if ( OpenVibeClientJS_IsRunning() )
- g_OVClientJS.CallHookVoid( "Shutdown" );
- g_OVClientJS.Shutdown();
- g_OVClientJSStarted = false;
+ if ( g_pClientCore )
+ {
+ ovjs_fire_hook( g_pClientCore, "Shutdown" );
+ ovjs_destroy( g_pClientCore );
+ g_pClientCore = NULL;
+ }
}
}
void OpenVibeJS_Client_Think()
void OpenVibeJS_Client_Think()
{
{
- if ( OpenVibeClientJS_IsRunning() )
- g_OVClientJS.CallHookVoid( "Think" );
+ if ( g_pClientCore )
+ ovjs_fire_hook( g_pClientCore, "Think" );
}
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-// Client concommands (mirror the server ones for parity/debugging).
+// Client concommands.
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
static void OV_ClientJSStatus_f()
static void OV_ClientJSStatus_f()
{
{
- Msg( "[OV JS/client] enabled=%d started=%d running=%d mode=%s\n",
+ Msg( "[OV JS/client] enabled=%d running=%d mode=%s\n",
ov_client_js_enabled.GetBool() ? 1 : 0,
ov_client_js_enabled.GetBool() ? 1 : 0,
- g_OVClientJSStarted ? 1 : 0,
- OpenVibeClientJS_IsRunning() ? 1 : 0,
+ g_pClientCore ? 1 : 0,
ov_client_mode.GetString() );
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,
@@ -320,33 +200,25 @@ static void OV_ClientJSReload_f()
@@ -320,33 +200,25 @@ static void OV_ClientJSReload_f()
static ConCommand ov_client_js_reload( "ov_client_js_reload", OV_ClientJSReload_f,
static ConCommand ov_client_js_reload( "ov_client_js_reload", OV_ClientJSReload_f,
"Reload the OpenVibe client JavaScript runtime.", FCVAR_CLIENTDLL );
"Reload the OpenVibe client JavaScript runtime.", FCVAR_CLIENTDLL );
-// Drives the client JS runtime: hooks the OVNet usermessage at load, starts the
-// runtime once the level/systems are up, and pumps Think() each frame (for JS
-// timers). Self-contained so no external init-call patching is needed.
+// Drives the runtime: hook OVNet at load, start on level enter, Think per frame.
class COpenVibeClientJSSystem : public CAutoGameSystemPerFrame
class COpenVibeClientJSSystem : public CAutoGameSystemPerFrame
{
{
public:
public:
COpenVibeClientJSSystem() : CAutoGameSystemPerFrame( "COpenVibeClientJSSystem" ) {}
COpenVibeClientJSSystem() : CAutoGameSystemPerFrame( "COpenVibeClientJSSystem" ) {}
-
bool Init() OVERRIDE
bool Init() OVERRIDE
{
{
usermessages->HookMessage( "OVNet", __MsgFunc_OVNet );
usermessages->HookMessage( "OVNet", __MsgFunc_OVNet );
return true;
return true;
}
}
-
void LevelInitPostEntity() OVERRIDE
void LevelInitPostEntity() OVERRIDE
{
{
- // (Re)start the runtime when a level is entered so <mode>/client.js and
- // client addons load fresh for the connected server.
OpenVibeJS_Client_Shutdown();
OpenVibeJS_Client_Shutdown();
OpenVibeJS_Client_Init();
OpenVibeJS_Client_Init();
}
}
-
void LevelShutdownPostEntity() OVERRIDE
void LevelShutdownPostEntity() OVERRIDE
{
{
OpenVibeJS_Client_Shutdown();
OpenVibeJS_Client_Shutdown();
}
}
-
void Update( float frametime ) OVERRIDE
void Update( float frametime ) OVERRIDE
{
{
OpenVibeJS_Client_Think();
OpenVibeJS_Client_Think();
new file mode 100644
new file mode 100644
@@ -0,0 +1,324 @@
@@ -0,0 +1,324 @@
+/* ovjs_core.c — host-agnostic OpenVibe JavaScript core. Compiled AS C so it can
+ * include QuickJS's C headers cleanly (see ovjs_core.h for the why). */
+#include "ovjs_core.h"
+#include "third_party/quickjs/quickjs.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+struct OVJSCore
+{
+ JSRuntime *rt;
+ JSContext *ctx;
+ const OVJSHost *host;
+ int isServer;
+ char mode[64];
+};
+
+/* The active core is stored as the context opaque so bindings can reach host. */
+static OVJSCore *OVJS_FromCtx(JSContext *ctx)
+{
+ return (OVJSCore *)JS_GetContextOpaque(ctx);
+}
+
+/* ------------------------------------------------------------------ */
+/* OV.* native bindings — thin shims over the host callbacks. */
+/* ------------------------------------------------------------------ */
+static JSValue ov_log(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ if (argc >= 1 && c && c->host && c->host->log) {
+ const char *s = JS_ToCString(ctx, argv[0]);
+ if (s) { c->host->log(s); JS_FreeCString(ctx, s); }
+ }
+ return JS_UNDEFINED;
+}
+static JSValue ov_warn(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ if (argc >= 1 && c && c->host && c->host->warn) {
+ const char *s = JS_ToCString(ctx, argv[0]);
+ if (s) { c->host->warn(s); JS_FreeCString(ctx, s); }
+ }
+ return JS_UNDEFINED;
+}
+static JSValue ov_error(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ if (argc >= 1 && c && c->host && c->host->error) {
+ const char *s = JS_ToCString(ctx, argv[0]);
+ if (s) { c->host->error(s); JS_FreeCString(ctx, s); }
+ }
+ return JS_UNDEFINED;
+}
+static JSValue ov_isServer(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ return JS_NewBool(ctx, c ? c->isServer : 0);
+}
+static JSValue ov_getMode(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ return JS_NewString(ctx, c ? c->mode : "");
+}
+static JSValue ov_getMapName(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ const char *m = (c && c->host && c->host->getMapName) ? c->host->getMapName() : "";
+ return JS_NewString(ctx, m ? m : "");
+}
+static JSValue ov_time(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ double v = (c && c->host && c->host->getTime) ? c->host->getTime() : 0.0;
+ return JS_NewFloat64(ctx, v);
+}
+static JSValue ov_readFile(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ if (argc < 1 || !c || !c->host || !c->host->readFile) return JS_NULL;
+ const char *path = JS_ToCString(ctx, argv[0]);
+ if (!path) return JS_NULL;
+ char *buf = c->host->readFile(path);
+ JS_FreeCString(ctx, path);
+ if (!buf) return JS_NULL;
+ JSValue out = JS_NewString(ctx, buf);
+ free(buf);
+ return out;
+}
+static JSValue ov_fileExists(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ if (argc < 1 || !c || !c->host || !c->host->fileExists) return JS_FALSE;
+ const char *path = JS_ToCString(ctx, argv[0]);
+ if (!path) return JS_FALSE;
+ int ok = c->host->fileExists(path);
+ JS_FreeCString(ctx, path);
+ return JS_NewBool(ctx, ok);
+}
+static JSValue ov_listDir(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ JSValue arr = JS_NewArray(ctx);
+ if (argc < 1 || !c || !c->host || !c->host->listDir) return arr;
+ const char *dir = JS_ToCString(ctx, argv[0]);
+ const char *wc = (argc >= 2) ? JS_ToCString(ctx, argv[1]) : NULL;
+ if (dir) {
+ char *joined = c->host->listDir(dir, (wc && wc[0]) ? wc : "*");
+ if (joined) {
+ uint32_t idx = 0;
+ char *p = joined, *start = joined;
+ for (;; ++p) {
+ if (*p == '\n' || *p == '\0') {
+ if (p > start) {
+ JS_SetPropertyUint32(ctx, arr, idx++,
+ JS_NewStringLen(ctx, start, (size_t)(p - start)));
+ }
+ if (*p == '\0') break;
+ start = p + 1;
+ }
+ }
+ free(joined);
+ }
+ }
+ if (dir) JS_FreeCString(ctx, dir);
+ if (wc) JS_FreeCString(ctx, wc);
+ return arr;
+}
+static JSValue ov_broadcast(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ /* Server-only chat broadcast is handled by the server bindings; on the
+ * client this is an inert stub so shared JS doesn't throw. */
+ return JS_UNDEFINED;
+}
+static JSValue ov_serverCommand(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ return JS_UNDEFINED;
+}
+static JSValue ov_netSendToServer(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ if (argc < 2 || !c || !c->host || !c->host->netSendToServer) return JS_UNDEFINED;
+ const char *name = JS_ToCString(ctx, argv[0]);
+ const char *pl = JS_ToCString(ctx, argv[1]);
+ if (name && pl) c->host->netSendToServer(name, pl);
+ if (name) JS_FreeCString(ctx, name);
+ if (pl) JS_FreeCString(ctx, pl);
+ return JS_UNDEFINED;
+}
+static JSValue ov_netEmit(JSContext *ctx, JSValueConst t, int argc, JSValueConst *argv)
+{
+ OVJSCore *c = OVJS_FromCtx(ctx);
+ if (argc < 3 || !c || !c->host || !c->host->netEmit) return JS_UNDEFINED;
+ const char *ids = JS_ToCString(ctx, argv[0]);
+ const char *name = JS_ToCString(ctx, argv[1]);
+ const char *pl = JS_ToCString(ctx, argv[2]);
+ if (ids && name && pl) c->host->netEmit(ids, name, pl);
+ if (ids) JS_FreeCString(ctx, ids);
+ if (name) JS_FreeCString(ctx, name);
+ if (pl) JS_FreeCString(ctx, pl);
+ return JS_UNDEFINED;
+}
+
+static const JSCFunctionListEntry ov_funcs[] = {
+ JS_CFUNC_DEF("log", 1, ov_log),
+ JS_CFUNC_DEF("warn", 1, ov_warn),
+ JS_CFUNC_DEF("error", 1, ov_error),
+ JS_CFUNC_DEF("isServer", 0, ov_isServer),
+ JS_CFUNC_DEF("getMode", 0, ov_getMode),
+ JS_CFUNC_DEF("getMapName", 0, ov_getMapName),
+ JS_CFUNC_DEF("time", 0, ov_time),
+ JS_CFUNC_DEF("readFile", 1, ov_readFile),
+ JS_CFUNC_DEF("fileExists", 1, ov_fileExists),
+ JS_CFUNC_DEF("listDir", 2, ov_listDir),
+ JS_CFUNC_DEF("broadcast", 1, ov_broadcast),
+ JS_CFUNC_DEF("serverCommand", 1, ov_serverCommand),
+ JS_CFUNC_DEF("netSendToServer", 2, ov_netSendToServer),
+ JS_CFUNC_DEF("netEmit", 3, ov_netEmit),
+};
+
+static void OVJS_ReportException(OVJSCore *c, const char *where)
+{
+ JSValue exc = JS_GetException(c->ctx);
+ const char *msg = JS_ToCString(c->ctx, exc);
+ if (c->host && c->host->error) {
+ char buf[1024];
+ snprintf(buf, sizeof(buf), "[%s] %s", where ? where : "eval", msg ? msg : "unknown");
+ c->host->error(buf);
+ }
+ if (msg) JS_FreeCString(c->ctx, msg);
+ JS_FreeValue(c->ctx, exc);
+}
+
+int ovjs_eval(OVJSCore *c, const char *code, const char *filename)
+{
+ if (!c || !c->ctx || !code) return 0;
+ JSValue r = JS_Eval(c->ctx, code, strlen(code),
+ filename ? filename : "<openvibe>", JS_EVAL_TYPE_GLOBAL);
+ if (JS_IsException(r)) {
+ OVJS_ReportException(c, filename);
+ JS_FreeValue(c->ctx, r);
+ return 0;
+ }
+ JS_FreeValue(c->ctx, r);
+ return 1;
+}
+
+static int OVJS_LoadFile(OVJSCore *c, const char *path)
+{
+ if (!c->host || !c->host->readFile) return 0;
+ char *code = c->host->readFile(path);
+ if (!code) {
+ if (c->host->warn) {
+ char buf[512];
+ snprintf(buf, sizeof(buf), "core: could not read %s", path);
+ c->host->warn(buf);
+ }
+ return 0;
+ }
+ int ok = ovjs_eval(c, code, path);
+ free(code);
+ return ok;
+}
+
+static int OVJS_LoadCoreFiles(OVJSCore *c)
+{
+ if (!OVJS_LoadFile(c, "js/core/hook.js")) return 0;
+ if (!OVJS_LoadFile(c, "js/core/gamemode.js")) return 0;
+ OVJS_LoadFile(c, "js/bridge.js"); /* bootstraps module/net/addon */
+ OVJS_LoadFile(c, "js/core/command.js");
+ OVJS_LoadFile(c, "js/core/timer.js");
+ return 1;
+}
+
+static void OVJS_LoadGamemode(OVJSCore *c)
+{
+ char path[256];
+ OVJS_LoadFile(c, "js/gamemodes/base/server.js"); /* base defines shared GM */
+ snprintf(path, sizeof(path), "js/gamemodes/%s/%s.js",
+ c->mode, c->isServer ? "server" : "client");
+ OVJS_LoadFile(c, path);
+}
+
+static void OVJS_RegisterBindings(OVJSCore *c)
+{
+ JSValue global = JS_GetGlobalObject(c->ctx);
+ JSValue ov = JS_NewObject(c->ctx);
+ JS_SetPropertyFunctionList(c->ctx, ov, ov_funcs,
+ (int)(sizeof(ov_funcs) / sizeof(ov_funcs[0])));
+ JS_SetPropertyStr(c->ctx, global, "OV", ov);
+ JS_FreeValue(c->ctx, global);
+}
+
+OVJSCore *ovjs_create(const OVJSHost *host, int isServerRealm, const char *mode)
+{
+ OVJSCore *c = (OVJSCore *)calloc(1, sizeof(OVJSCore));
+ if (!c) return NULL;
+ c->host = host;
+ c->isServer = isServerRealm ? 1 : 0;
+ if (mode && mode[0]) { strncpy(c->mode, mode, sizeof(c->mode) - 1); }
+ else { strncpy(c->mode, "hub", sizeof(c->mode) - 1); }
+
+ c->rt = JS_NewRuntime();
+ if (!c->rt) { free(c); return NULL; }
+ JS_SetMemoryLimit(c->rt, 32 * 1024 * 1024);
+ JS_SetMaxStackSize(c->rt, 1024 * 1024);
+
+ c->ctx = JS_NewContext(c->rt);
+ if (!c->ctx) { JS_FreeRuntime(c->rt); free(c); return NULL; }
+ JS_SetContextOpaque(c->ctx, c);
+
+ OVJS_RegisterBindings(c);
+ if (!OVJS_LoadCoreFiles(c)) { ovjs_destroy(c); return NULL; }
+ OVJS_LoadGamemode(c);
+ return c;
+}
+
+void ovjs_destroy(OVJSCore *c)
+{
+ if (!c) return;
+ if (c->ctx) JS_FreeContext(c->ctx);
+ if (c->rt) JS_FreeRuntime(c->rt);
+ free(c);
+}
+
+/* Fire gamemode.call(hook, args...) — mirrors the server bridge's hook dispatch. */
+static void OVJS_FireHookArgv(OVJSCore *c, const char *hook, int argc, JSValueConst *argv)
+{
+ JSValue global = JS_GetGlobalObject(c->ctx);
+ JSValue gamemode = JS_GetPropertyStr(c->ctx, global, "gamemode");
+ JSValue call = JS_GetPropertyStr(c->ctx, gamemode, "call");
+ if (JS_IsFunction(c->ctx, call)) {
+ int n = argc + 1;
+ JSValue *args = (JSValue *)malloc(sizeof(JSValue) * (n > 0 ? n : 1));
+ args[0] = JS_NewString(c->ctx, hook);
+ for (int i = 0; i < argc; ++i) args[i + 1] = argv[i];
+ JSValue r = JS_Call(c->ctx, call, gamemode, n, args);
+ if (JS_IsException(r)) OVJS_ReportException(c, hook);
+ JS_FreeValue(c->ctx, r);
+ JS_FreeValue(c->ctx, args[0]);
+ free(args);
+ }
+ JS_FreeValue(c->ctx, call);
+ JS_FreeValue(c->ctx, gamemode);
+ JS_FreeValue(c->ctx, global);
+}
+
+void ovjs_fire_hook(OVJSCore *c, const char *hook)
+{
+ if (!c || !c->ctx || !hook) return;
+ OVJS_FireHookArgv(c, hook, 0, NULL);
+}
+
+void ovjs_fire_hook_s(OVJSCore *c, const char *hook,
+ const char *a, const char *b, const char *d)
+{
+ if (!c || !c->ctx || !hook) return;
+ JSValue argv[3];
+ int argc = 0;
+ if (a) { argv[argc++] = JS_NewString(c->ctx, a); }
+ if (a && b) { argv[argc++] = JS_NewString(c->ctx, b); }
+ if (a && b && d) { argv[argc++] = JS_NewString(c->ctx, d); }
+ OVJS_FireHookArgv(c, hook, argc, argv);
+ for (int i = 0; i < argc; ++i) JS_FreeValue(c->ctx, argv[i]);
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
+/* ovjs_core.h — host-agnostic OpenVibe JavaScript core (C ABI).
+ *
+ * This is the boundary that lets the client DLL embed QuickJS on Windows.
+ * The core (ovjs_core.c) is compiled AS C, so it may include quickjs.h — whose
+ * C99 compound literals / designated initializers are valid in C but rejected by
+ * MSVC cl.exe in C++ mode. The Source-SDK glue (openvibe_js_client.cpp) is C++
+ * and includes ONLY this pure-C-ABI header, never quickjs.h, so cl.exe never
+ * parses QuickJS. The core reaches the engine (logging, files, networking)
+ * exclusively through the host callbacks below.
+ */
+#ifndef OVJS_CORE_H
+#define OVJS_CORE_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct OVJSCore OVJSCore;
+
+/* Callbacks the JS core invokes; implemented by the host (Source SDK glue).
+ * String returns marked "(host-owned static)" must remain valid until the next
+ * call. readFile/listDir return malloc()'d buffers the CORE frees with free(). */
+typedef struct OVJSHost
+{
+ void (*log)(const char *msg);
+ void (*warn)(const char *msg);
+ void (*error)(const char *msg);
+
+ /* Returns malloc'd NUL-terminated file contents (core frees), or NULL. */
+ char *(*readFile)(const char *path);
+ int (*fileExists)(const char *path);
+ /* Returns malloc'd newline-joined entry names (core frees), or NULL. */
+ char *(*listDir)(const char *dir, const char *wildcard);
+
+ int (*isServer)(void);
+ const char *(*getMode)(void); /* host-owned static */
+ const char *(*getMapName)(void); /* host-owned static */
+ double (*getTime)(void);
+
+ /* client -> server */
+ void (*netSendToServer)(const char *name, const char *payloadB64);
+ /* server -> client (idsCsv: comma-separated userIds, "-1" = broadcast) */
+ void (*netEmit)(const char *idsCsv, const char *name, const char *payloadB64);
+} OVJSHost;
+
+/* Create a runtime, register the OV.* bindings, and load the core JS files plus
+ * js/gamemodes/<mode>/(server|client).js. Returns NULL on failure. */
+OVJSCore *ovjs_create(const OVJSHost *host, int isServerRealm, const char *mode);
+void ovjs_destroy(OVJSCore *core);
+
+/* Evaluate a script. Returns 1 on success, 0 on error (reported via host->error). */
+int ovjs_eval(OVJSCore *core, const char *code, const char *filename);
+
+/* Fire a gamemode/hook by name. The _s variant passes up to three string args
+ * (pass NULL to stop early); used for events like OVNetReceive(name,payload). */
+void ovjs_fire_hook(OVJSCore *core, const char *hook);
+void ovjs_fire_hook_s(OVJSCore *core, const char *hook,
+ const char *a, const char *b, const char *c);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* OVJS_CORE_H */
@@ -54,6 +54,10 @@ copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.h" \
@@ -54,6 +54,10 @@ copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.h" \
"$SDK/src/game/shared/openvibe/ov_js_player.h"
"$SDK/src/game/shared/openvibe/ov_js_player.h"
copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.cpp" \
copy_file "$ROOT/sdk/openvibe/shared/ov_js_player.cpp" \
"$SDK/src/game/shared/openvibe/ov_js_player.cpp"
"$SDK/src/game/shared/openvibe/ov_js_player.cpp"
+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/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"
@@ -79,8 +83,9 @@ perl -0pi -e '
@@ -79,8 +83,9 @@ perl -0pi -e '
s/^.*hl2mp\\vgui_openvibe_menu\.cpp.*\n//mg;
s/^.*hl2mp\\vgui_openvibe_menu\.cpp.*\n//mg;
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/^.*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\\ov_js_runtime.cpp"\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\\ovjs_core.c" [\$LINUXALL]\n\t\t\t\$Lib\t"..\\shared\\openvibe\\third_party\\quickjs\\build\\libquickjs_openvibe"\n/s;
' "$CLIENT_VPC"
' "$CLIENT_VPC"
echo "[openvibe-sdk] patched client_hl2mp.vpc"
echo "[openvibe-sdk] patched client_hl2mp.vpc"
@@ -282,6 +282,25 @@ foreach ($src in $sources) {
@@ -282,6 +282,25 @@ foreach ($src in $sources) {
$objects += $obj
$objects += $obj
}
}
+# Also compile the OpenVibe host-agnostic JS core (ovjs_core.c) with the SAME
+# clang-cl toolchain and pack it into libquickjs_openvibe.lib. ovjs_core.c is C
+# and includes quickjs.h, so it must NOT be compiled by cl.exe in C++ mode — this
+# is exactly why the client glue talks to it through the plain C ABI in
+# ovjs_core.h. Compiling it here means the client project just links this lib.
+$SdkOpenvibe = Join-Path $Sdk 'src/game/shared/openvibe'
+$OvjsCoreSrc = Join-Path $SdkOpenvibe 'ovjs_core.c'
+if (Test-Path $OvjsCoreSrc) {
+ $ovjsObj = Join-Path $Out 'ovjs_core.obj'
+ $CoreArgs = $Common + @("/I$SdkOpenvibe")
+ Say "$([IO.Path]::GetFileName($Compiler)) ovjs_core.c -> $ovjsObj"
+ & $Compiler @CoreArgs /c "$OvjsCoreSrc" "/Fo$ovjsObj"
+ if ($LASTEXITCODE -ne 0) { throw "$Compiler failed for ovjs_core.c" }
+ if (!(Test-Path $ovjsObj)) { throw "$Compiler reported success but did not produce $ovjsObj" }
+ $objects += $ovjsObj
+} else {
+ Say "ovjs_core.c not present at $OvjsCoreSrc; skipping (client JS core will be unresolved)"
+}
+
$outLib = Join-Path $Out 'libquickjs_openvibe.lib'
$outLib = Join-Path $Out 'libquickjs_openvibe.lib'
Say "lib -> $outLib"
Say "lib -> $outLib"
& lib.exe /nologo "/OUT:$outLib" $objects
& lib.exe /nologo "/OUT:$outLib" $objects