openvibers/source.openvibe.games Commits

Add cross-platform IPC client (ov_ipc) + runtime launcher

CO Copilot committed 2 Jul 2026 · a5c6ea40b8 · 3 files, +210 −0

ov_ipc.{h,cpp}: minimal non-blocking line-delimited TCP JSON client for
bridging the game DLLs to the Node.js runtime (server 41999 / client
41998), with auto-reconnect. Winsock on Windows, BSD sockets on Linux.
tools/run-js-runtime.sh launches a realm's Node runtime host.
Next: wire server.so + client.dll event forwarding through this bridge.

Changes

ddiff --git a/sdk/openvibe/shared/ov_ipc.cpp b/sdk/openvibe/shared/ov_ipc.cpp
nnew file mode 100644
iindex 0000000..8ed6b98
---- /dev/null
++++ b/sdk/openvibe/shared/ov_ipc.cpp
@@@ -0,0 +1,153 @@
++#include "cbase.h"
++#include "ov_ipc.h"
++
++#include <string.h>
++
++#ifdef _WIN32
++#include <winsock2.h>
++#include <ws2tcpip.h>
++typedef int socklen_t;
++#define OVIPC_INVALID INVALID_SOCKET
++#define ovipc_close closesocket
++static bool s_wsaInit = false;
++static void OVIPC_PlatformInit()
++{
++ if ( !s_wsaInit ) { WSADATA w; WSAStartup( MAKEWORD( 2, 2 ), &w ); s_wsaInit = true; }
++}
++static bool OVIPC_WouldBlock() { return WSAGetLastError() == WSAEWOULDBLOCK; }
++static void OVIPC_SetNonBlocking( int s ) { u_long m = 1; ioctlsocket( s, FIONBIO, &m ); }
++#else
++#include <sys/socket.h>
++#include <netinet/in.h>
++#include <netinet/tcp.h>
++#include <arpa/inet.h>
++#include <fcntl.h>
++#include <unistd.h>
++#include <errno.h>
++#define OVIPC_INVALID (-1)
++#define ovipc_close ::close
++static void OVIPC_PlatformInit() {}
++static bool OVIPC_WouldBlock() { return errno == EWOULDBLOCK || errno == EAGAIN; }
++static void OVIPC_SetNonBlocking( int s ) { int f = fcntl( s, F_GETFL, 0 ); fcntl( s, F_SETFL, f | O_NONBLOCK ); }
++#endif
++
++#include "tier0/memdbgon.h"
++
++COpenVibeIPC::COpenVibeIPC()
++ : m_nPort( 0 ), m_LineCb( NULL ), m_Socket( OVIPC_INVALID ),
++ m_flNextReconnect( 0.0 ), m_RxLen( 0 )
++{
++ m_szHost[0] = '\0';
++ OVIPC_PlatformInit();
++}
++
++COpenVibeIPC::~COpenVibeIPC()
++{
++ Close();
++}
++
++void COpenVibeIPC::Configure( const char *pszHost, int nPort, LineFn lineCb )
++{
++ Q_strncpy( m_szHost, pszHost && pszHost[0] ? pszHost : "127.0.0.1", sizeof( m_szHost ) );
++ m_nPort = nPort;
++ m_LineCb = lineCb;
++}
++
++bool COpenVibeIPC::IsConnected() const
++{
++ return m_Socket != OVIPC_INVALID;
++}
++
++void COpenVibeIPC::Close()
++{
++ if ( m_Socket != OVIPC_INVALID )
++ {
++ ovipc_close( m_Socket );
++ m_Socket = OVIPC_INVALID;
++ }
++ m_RxLen = 0;
++}
++
++bool COpenVibeIPC::TryConnect()
++{
++ if ( m_nPort <= 0 ) return false;
++
++ int s = (int)socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
++ if ( s == OVIPC_INVALID ) return false;
++
++ struct sockaddr_in addr;
++ memset( &addr, 0, sizeof( addr ) );
++ addr.sin_family = AF_INET;
++ addr.sin_port = htons( (unsigned short)m_nPort );
++ addr.sin_addr.s_addr = inet_addr( m_szHost );
++
++ // Blocking connect to localhost is effectively instant; then go non-blocking.
++ if ( connect( s, (struct sockaddr *)&addr, sizeof( addr ) ) != 0 )
++ {
++ ovipc_close( s );
++ return false;
++ }
++
++ int one = 1;
++ setsockopt( s, IPPROTO_TCP, TCP_NODELAY, (const char *)&one, sizeof( one ) );
++ OVIPC_SetNonBlocking( s );
++
++ m_Socket = s;
++ m_RxLen = 0;
++ return true;
++}
++
++bool COpenVibeIPC::SendLine( const char *pszJson )
++{
++ if ( !pszJson ) return false;
++ if ( m_Socket == OVIPC_INVALID ) return false;
++
++ int len = Q_strlen( pszJson );
++ bool needNL = ( len == 0 || pszJson[len - 1] != '\n' );
++
++ // Blocking-ish send to localhost; small messages. On error, drop + close.
++ int sent = send( m_Socket, pszJson, len, 0 );
++ if ( sent < 0 ) { if ( !OVIPC_WouldBlock() ) Close(); return false; }
++ if ( needNL ) send( m_Socket, "\n", 1, 0 );
++ return true;
++}
++
++void COpenVibeIPC::Poll()
++{
++ if ( m_Socket == OVIPC_INVALID )
++ {
++ double now = Plat_FloatTime();
++ if ( now < m_flNextReconnect ) return;
++ m_flNextReconnect = now + 2.0; // retry every 2s
++ if ( !TryConnect() ) return;
++ }
++
++ for (;;)
++ {
++ int space = (int)sizeof( m_RxBuf ) - 1 - m_RxLen;
++ if ( space <= 0 ) { m_RxLen = 0; space = (int)sizeof( m_RxBuf ) - 1; } // overflow guard
++ int n = recv( m_Socket, m_RxBuf + m_RxLen, space, 0 );
++ if ( n == 0 ) { Close(); return; } // peer closed
++ if ( n < 0 ) { if ( !OVIPC_WouldBlock() ) Close(); break; }
++ m_RxLen += n;
++ m_RxBuf[m_RxLen] = '\0';
++
++ // Dispatch complete lines.
++ int start = 0;
++ for ( int i = 0; i < m_RxLen; ++i )
++ {
++ if ( m_RxBuf[i] == '\n' )
++ {
++ m_RxBuf[i] = '\0';
++ if ( m_LineCb && i > start ) m_LineCb( m_RxBuf + start );
++ start = i + 1;
++ }
++ }
++ if ( start > 0 )
++ {
++ m_RxLen -= start;
++ memmove( m_RxBuf, m_RxBuf + start, m_RxLen );
++ }
++ if ( n < space ) break; // drained
++ }
++}
ddiff --git a/sdk/openvibe/shared/ov_ipc.h b/sdk/openvibe/shared/ov_ipc.h
nnew file mode 100644
iindex 0000000..0f0da5d
---- /dev/null
++++ b/sdk/openvibe/shared/ov_ipc.h
@@@ -0,0 +1,45 @@
++#ifndef OV_IPC_H
++#define OV_IPC_H
++#ifdef _WIN32
++#pragma once
++#endif
++
++// Minimal cross-platform, non-blocking, line-delimited TCP client used to bridge
++// the Source game DLLs to the OpenVibe Node.js runtime host (ov-runtime.js).
++// Each realm connects to its runtime port (server 41999, client 41998), sends
++// newline-terminated JSON messages, and polls for inbound newline-terminated
++// JSON which it hands to a line callback. Auto-reconnects when the runtime isn't
++// up yet. No JSON parsing here — callers build/parse the JSON.
++class COpenVibeIPC
++{
++public:
++ typedef void (*LineFn)( const char *pszLine );
++
++ COpenVibeIPC();
++ ~COpenVibeIPC();
++
++ // host is usually "127.0.0.1". lineCb is invoked (during Poll) per inbound line.
++ void Configure( const char *pszHost, int nPort, LineFn lineCb );
++
++ bool IsConnected() const;
++ // Send one message; a trailing '\n' is added if missing. Returns false if not
++ // connected (message dropped). Attempts a (throttled) reconnect if needed.
++ bool SendLine( const char *pszJson );
++ // Pump: attempt reconnect if down, read available bytes, dispatch full lines.
++ // Call every frame/tick.
++ void Poll();
++ void Close();
++
++private:
++ bool TryConnect();
++
++ char m_szHost[64];
++ int m_nPort;
++ LineFn m_LineCb;
++ int m_Socket; // -1 when not connected
++ double m_flNextReconnect; // throttle
++ char m_RxBuf[65536];
++ int m_RxLen;
++};
++
++#endif // OV_IPC_H
ddiff --git a/tools/run-js-runtime.sh b/tools/run-js-runtime.sh
nnew file mode 100755
iindex 0000000..24b9e71
---- /dev/null
++++ b/tools/run-js-runtime.sh
@@@ -0,0 +1,12 @@
++#!/usr/bin/env bash
++# Launch an OpenVibe Node.js JS runtime host for a realm.
++# tools/run-js-runtime.sh server sandbox # server realm, sandbox mode
++# tools/run-js-runtime.sh client sandbox # client realm
++# Ports: server=41999, client=41998 (override with OV_JS_PORT).
++set -euo pipefail
++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
++REALM="${1:-server}"
++MODE="${2:-sandbox}"
++if [[ "$REALM" == "client" ]]; then PORT="${OV_JS_PORT:-41998}"; else PORT="${OV_JS_PORT:-41999}"; fi
++exec node "$ROOT/engine/openvibe-js-runtime/ov-runtime.js" \
++ --realm "$REALM" --mode "$MODE" --port "$PORT" --root "$ROOT"