openvibers/source.openvibe.games Commits

Fix local server connectivity + full console overhaul (engine everywhere, autocomplete, menu override)

CO Copilot committed 3 Jul 2026 · 015db5bdd5 · 11 files, +398 −27

Connectivity (Connection failed after 4 retries):
- sv_lan 0 everywhere: LAN mode kills Steam GS logon and this engine then
  drops all unconnected packets (no query replies, no connect challenges)
- client: drop -insecure (VAC-secured servers reject insecure clients with
  'You are in insecure mode'); dev servers run -insecure instead
- client: +net_usesocketsforloopback 1 (loopback connects to a separate
  local dedicated server otherwise never leave the process)
- srcds gets steamcmd's steamclient.so (desktop-client fallback wedges the
  GS interface with 'invalid hSteamUser' assertions)
- sv_master_share_game_socket 0 for local shards

Console:
- openvibe://cmd from the local UI runs ANY engine command line (like the
  stock console; ov_console_unrestricted + page-origin gate; remote pages
  keep the allowlist); commands echo '] cmd' into the tap
- engine console lines stream to the runtime ({t:conline}) and out the SSE
  log feed, so the GUI console shows engine output in every host
- client JS bridge connects at boot (client realm works at the main menu)
- OpenVibe_MenuKeepAlive: the HTML menu auto-reopens over the stock GameUI
  whenever out of a level — the stock menu can no longer take over
- GUI console autocomplete: engine commands + live map list (/maps) +
  js/ script list (/scripts) + ov_join modes + JS globals; Tab/arrows
- runtime /exec reports 'game not connected' honestly

Co-Authored-By: Claude Fable 5 <[email protected]>

Changes

ddiff --git a/client/index.html b/client/index.html
iindex 70841a8..c577b9d 100644
---- a/client/index.html
++++ b/client/index.html
@@@ -601,6 +601,19 @@
font-size: var(--console-font);
}
.console-input:focus { outline: none; border-color: var(--accent); }
++ .console-input-wrap { position: relative; }
++ .console-suggest {
++ position: absolute; bottom: 100%; left: 24px; right: 70px; margin-bottom: 4px;
++ background: var(--bg-elev, #101826); border: 1px solid var(--border, #24324a);
++ border-radius: 6px; max-height: 220px; overflow-y: auto; z-index: 40;
++ font-family: var(--mono, monospace); font-size: var(--console-font, 13px);
++ box-shadow: 0 -4px 18px rgba(0,0,0,0.45);
++ }
++ .console-suggest .sugg {
++ padding: 4px 10px; cursor: pointer; display: flex; justify-content: space-between; gap: 12px;
++ }
++ .console-suggest .sugg .hint { opacity: 0.55; font-size: 0.85em; }
++ .console-suggest .sugg.sel, .console-suggest .sugg:hover { background: rgba(80,140,255,0.18); }
.console-select {
background: var(--bg-card);
border: 1px solid var(--border);
@@@ -1041,11 +1054,14 @@
<button class="btn-console" onclick="OVApp.consoleClear()">Clear</button>
</div>
<div id="console-log" class="console-log" aria-live="polite"></div>
-- <div class="console-input-row">
-- <span class="console-prompt">›</span>
-- <input id="console-input" class="console-input" type="text" spellcheck="false" autocomplete="off"
-- placeholder="JS code — or ]engine_command — or npm install <pkg>" />
-- <button class="btn-console" onclick="OVApp.consoleSubmit()">Run</button>
++ <div class="console-input-wrap">
++ <div id="console-suggest" class="console-suggest" hidden></div>
++ <div class="console-input-row">
++ <span class="console-prompt">›</span>
++ <input id="console-input" class="console-input" type="text" spellcheck="false" autocomplete="off"
++ placeholder="JS code — or ]engine_command — or npm install <pkg> — Tab completes" />
++ <button class="btn-console" onclick="OVApp.consoleSubmit()">Run</button>
++ </div>
</div>
</div>
</div>
@@@ -1482,24 +1498,177 @@
input.addEventListener('keydown', function (ev) {
if (ev.key === 'Enter') {
ev.preventDefault();
++ if (_sugg.open && _sugg.sel >= 0) { suggApply(); return; }
++ suggHide();
consoleSubmit();
++ } else if (ev.key === 'Tab') {
++ ev.preventDefault();
++ if (_sugg.open) suggApply();
++ else suggUpdate(true);
++ } else if (ev.key === 'Escape') {
++ if (_sugg.open) { ev.preventDefault(); suggHide(); }
} else if (ev.key === 'ArrowUp') {
ev.preventDefault();
++ if (_sugg.open) { suggMove(-1); return; }
if (_con.hIdx > 0) {
_con.hIdx -= 1;
input.value = _con.history[_con.hIdx] || '';
}
} else if (ev.key === 'ArrowDown') {
ev.preventDefault();
++ if (_sugg.open) { suggMove(1); return; }
if (_con.hIdx < _con.history.length) {
_con.hIdx += 1;
input.value = _con.history[_con.hIdx] || '';
}
}
});
++ input.addEventListener('input', function () { suggUpdate(false); });
++ input.addEventListener('blur', function () { setTimeout(suggHide, 150); });
++ suggPrefetch();
}
}
++ // ── Console autocomplete ────────────────────────────────────────────────
++ // Engine commands + convars (curated), live map list (/maps) and script
++ // list (/scripts) from the server runtime, ov_join modes, JS globals.
++ var ENGINE_COMMANDS = [
++ ['map', 'load a map (dedicated-style)'], ['changelevel', 'switch map, keep players'],
++ ['connect', 'join server ip:port'], ['disconnect', 'leave server'], ['retry', 'reconnect last server'],
++ ['status', 'server/player status'], ['ping', 'show latency'], ['kill', 'suicide'], ['explode', 'explode'],
++ ['noclip', 'toggle fly/clip (sv_cheats 1)'], ['sv_cheats', '0/1 enable cheats'], ['god', 'godmode (cheat)'],
++ ['echo', 'print text'], ['say', 'chat message'], ['name', 'set player name'],
++ ['bind', 'bind <key> <cmd>'], ['unbind', 'unbind <key>'], ['fov_desired', 'field of view'],
++ ['sensitivity', 'mouse sensitivity'], ['volume', 'master volume 0-1'], ['snd_musicvolume', 'music volume'],
++ ['net_graph', '0-3 net debug HUD'], ['cl_showfps', '0-2 fps meter'], ['quit', 'exit game'],
++ ['ov_join', 'join an OpenVibe mode'], ['ov_menu', 'open the OpenVibe menu'], ['ov_console', 'toggle this console'],
++ ['ov_menu_console', 'console route'], ['ov_menu_options', 'options route'], ['ov_menu_close', 'close menu'],
++ ['ov_js_status', 'server JS runtime status'], ['ov_js_reload', 'reload server JS'],
++ ['ov_js_fire', 'fire a JS hook'], ['ov_js_cmd', 'run a JS chat/console command'],
++ ['ov_npm', 'npm install/ls in js/'], ['js_run', 'eval JS on the server'], ['js_run_cl', 'eval JS on this client'],
++ ['js_openscript', 'run js/<path> on the server'], ['js_openscript_cl', 'run js/<path> on this client'],
++ ['ov_client_js_status', 'client JS bridge status'], ['ov_client_js_reconnect', 'reconnect client JS bridge'],
++ ['ov_prophunt_disguise', 'disguise as prop'], ['ov_fortwars_spawn', 'spawn build prop']
++ ];
++ var JS_GLOBALS = ['hook.Add(', 'hook.Run(', 'hook.Remove(', 'net.Start(', 'net.Receive(', 'net.Broadcast()',
++ 'net.SendToServer()', 'ents.Create(', 'ents.GetAll()', 'ents.FindByClass(', 'player.GetAll()',
++ 'player.GetByUserID(', 'team.GetAllTeams()', 'timer.Simple(', 'timer.Create(', 'GAMEMODE', 'LocalPlayer()',
++ 'util.AddNetworkString(', 'scripted_ents.Register(', 'concommand.Add(', 'include(', 'AddCSJSFile(',
++ 'require(', 'OV.log(', 'OV.getMode()', 'OVLoader.openScript(', 'RunConsoleCommand('];
++ var OV_MODES = ['hub', 'prophunt', 'deathrun', 'fortwars', 'traitortown', 'sandbox'];
++ var _sugg = { open: false, sel: -1, items: [], maps: null, scripts: null };
++
++ function suggPrefetch() {
++ fetch(RT.server + '/maps').then(function (r) { return r.json(); })
++ .then(function (res) { _sugg.maps = res.maps || []; }).catch(function () { _sugg.maps = []; });
++ fetch(RT.server + '/scripts').then(function (r) { return r.json(); })
++ .then(function (res) { _sugg.scripts = res.scripts || []; }).catch(function () { _sugg.scripts = []; });
++ }
++
++ function suggCandidates(line, realm) {
++ var isEngine = realm === 'engine' || line.charAt(0) === ']';
++ var text = line.charAt(0) === ']' ? line.slice(1) : line;
++ var parts = text.split(/\s+/);
++ var word = parts[parts.length - 1];
++ var head = parts[0] ? parts[0].toLowerCase() : '';
++ var out = [];
++
++ if (isEngine && parts.length >= 2) {
++ // Argument completion for known commands.
++ var pool = null, hint = '';
++ if ((head === 'map' || head === 'changelevel') && _sugg.maps) { pool = _sugg.maps; hint = 'map'; }
++ else if (head === 'ov_join') { pool = OV_MODES; hint = 'mode'; }
++ else if ((head === 'js_openscript' || head === 'js_openscript_cl') && _sugg.scripts) { pool = _sugg.scripts; hint = 'js/'; }
++ if (pool) {
++ pool.forEach(function (m) {
++ if (m.toLowerCase().indexOf(word.toLowerCase()) === 0 && out.length < 10) {
++ out.push({ text: m, hint: hint, replaceWord: true });
++ }
++ });
++ }
++ return out;
++ }
++
++ if (isEngine) {
++ ENGINE_COMMANDS.forEach(function (c) {
++ if (c[0].indexOf(word.toLowerCase()) === 0 && word.length > 0 && out.length < 10) {
++ out.push({ text: c[0], hint: c[1], replaceWord: true });
++ }
++ });
++ return out;
++ }
++
++ // JS realms: complete known globals; npm lines: subcommands.
++ if (/^npm\s+\S*$/.test(text)) {
++ ['install', 'uninstall', 'update', 'ls'].forEach(function (s) {
++ if (s.indexOf(word.toLowerCase()) === 0) out.push({ text: s, hint: 'npm', replaceWord: true });
++ });
++ return out;
++ }
++ if (word.length > 1) {
++ JS_GLOBALS.forEach(function (g) {
++ if (g.toLowerCase().indexOf(word.toLowerCase()) === 0 && out.length < 10) {
++ out.push({ text: g, hint: 'js', replaceWord: true });
++ }
++ });
++ }
++ return out;
++ }
++
++ function suggUpdate(force) {
++ var input = document.getElementById('console-input');
++ var box = document.getElementById('console-suggest');
++ if (!input || !box) return;
++ var line = input.value;
++ if (!line.trim() && !force) { suggHide(); return; }
++ var realm = (document.getElementById('console-realm') || {}).value || 'server';
++ var items = suggCandidates(line, realm);
++ if (!items.length) { suggHide(); return; }
++ _sugg.items = items;
++ _sugg.sel = 0;
++ _sugg.open = true;
++ box.innerHTML = '';
++ items.forEach(function (it, i) {
++ var d = document.createElement('div');
++ d.className = 'sugg' + (i === 0 ? ' sel' : '');
++ var t = document.createElement('span'); t.textContent = it.text;
++ var h = document.createElement('span'); h.className = 'hint'; h.textContent = it.hint || '';
++ d.appendChild(t); d.appendChild(h);
++ d.addEventListener('mousedown', function (ev) { ev.preventDefault(); _sugg.sel = i; suggApply(); });
++ box.appendChild(d);
++ });
++ box.hidden = false;
++ }
++
++ function suggMove(dir) {
++ if (!_sugg.open || !_sugg.items.length) return;
++ _sugg.sel = (_sugg.sel + dir + _sugg.items.length) % _sugg.items.length;
++ var box = document.getElementById('console-suggest');
++ if (!box) return;
++ Array.prototype.forEach.call(box.children, function (el, i) {
++ el.classList.toggle('sel', i === _sugg.sel);
++ if (i === _sugg.sel) el.scrollIntoView({ block: 'nearest' });
++ });
++ }
++
++ function suggApply() {
++ var input = document.getElementById('console-input');
++ if (!input || !_sugg.open || _sugg.sel < 0) return;
++ var it = _sugg.items[_sugg.sel];
++ var line = input.value;
++ var m = line.match(/^(.*?)(\S*)$/);
++ input.value = (m ? m[1] : '') + it.text + (it.hint === 'js' ? '' : ' ');
++ suggHide();
++ input.focus();
++ }
++
++ function suggHide() {
++ _sugg.open = false;
++ _sugg.sel = -1;
++ var box = document.getElementById('console-suggest');
++ if (box) box.hidden = true;
++ }
++
function conEval(realm, code) {
// Runtime control server offline? In-game we go through the engine bridge:
// js_run / js_run_cl — the result comes back through the console spew tap.
@@@ -1536,9 +1705,9 @@
function conEngine(cmd) {
if (!cmd) return;
if (embedded && shell === 'source') {
-- // In-game: allowlisted engine commands via the openvibe:// bridge.
++ // In-game: full console via the openvibe:// bridge — the command echoes
++ // and its output streams back through the engine console tap.
location.href = 'openvibe://cmd?c=' + encodeURIComponent(cmd);
-- conAppend('engine', 'sent: ' + cmd, 'ok');
return;
}
// Electron / browser: forward through the server runtime control server.
@@@ -1547,7 +1716,13 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command: cmd })
}).then(function (r) { return r.json(); }).then(function (res) {
-- conAppend('engine', typeof res === 'string' ? res : JSON.stringify(res), 'ok');
++ if (res && res.ok === false) {
++ conAppend('engine', res.error || 'command not handled', 'err');
++ } else if (res && res.handled === true) {
++ conAppend('engine', 'handled by JS command registry', 'ok');
++ } else {
++ conAppend('engine', 'forwarded to game', 'ok');
++ }
}).catch(function (e) {
conAppend('engine', 'exec failed (' + RT.server + '): ' + e.message, 'err');
});
ddiff --git a/engine/openvibe-js-runtime/ov-runtime.js b/engine/openvibe-js-runtime/ov-runtime.js
iindex 6a85277..0c98ebb 100644
---- a/engine/openvibe-js-runtime/ov-runtime.js
++++ b/engine/openvibe-js-runtime/ov-runtime.js
@@@ -278,6 +278,11 @@ function onGameMessage(msg) {
case 'think':
if (ctx && ctx.hook) { try { ctx.hook.Run('Think'); } catch {} }
break;
++ case 'conline':
++ // Engine console line mirrored from the game client's spew tap —
++ // republish on the SSE log stream so every GUI console host sees it.
++ if (msg.line) pushLog('engine', String(msg.line));
++ break;
case 'eval':
// js_run / js_run_cl from the game console.
if (ctx && ctx.OVLoader && typeof ctx.OVLoader.runString === 'function') {
@@@ -409,6 +414,10 @@ const ctrl = http.createServer(async (req, res) => {
} catch (e) { return json(res, 200, { ok: false, error: e.message }); }
if (!handled) {
// Forward to the engine (the DLL applies its ov_*/say allowlist).
++ if (!sock || sock.destroyed) {
++ pushLog('warn', `engine command dropped (game not connected to this runtime): ${line}`);
++ return json(res, 200, { ok: false, handled: false, error: 'game not connected to the ' + REALM + ' runtime — in-game use the console directly; the engine realm needs ov_js_backend node for launcher-side forwarding' });
++ }
sendToGame({ t: 'concmd', cmd: line });
handled = 'forwarded';
}
@@@ -419,6 +428,33 @@ const ctrl = http.createServer(async (req, res) => {
const args = Array.isArray(body.args) ? body.args.map(String) : [];
return runNpm(args, (result) => json(res, result.ok ? 200 : 400, result));
}
++ if (req.method === 'GET' && url.pathname === '/maps') {
++ let maps = [];
++ try {
++ maps = fs.readdirSync(path.join(MOD, 'maps'))
++ .filter((f) => f.endsWith('.bsp'))
++ .map((f) => f.replace(/\.bsp$/, ''))
++ .sort();
++ } catch { /* no maps dir */ }
++ return json(res, 200, { ok: true, maps });
++ }
++ if (req.method === 'GET' && url.pathname === '/scripts') {
++ // js/-relative script paths for js_openscript autocomplete.
++ const out = [];
++ const walk = (dir, rel, depth) => {
++ if (depth > 3) return;
++ let entries = [];
++ try { entries = fs.readdirSync(path.join(JS, dir), { withFileTypes: true }); } catch { return; }
++ for (const e of entries) {
++ if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
++ const r = rel ? rel + '/' + e.name : e.name;
++ if (e.isDirectory()) walk(dir + '/' + e.name, r, depth + 1);
++ else if (e.name.endsWith('.js')) out.push(r);
++ }
++ };
++ walk('.', '', 0);
++ return json(res, 200, { ok: true, scripts: out.sort().slice(0, 500) });
++ }
if (req.method === 'POST' && url.pathname === '/openscript') {
const body = await readBody(req);
const p = String(body.path || '');
ddiff --git a/game/openvibe.games/cfg/openvibe_client_default.cfg b/game/openvibe.games/cfg/openvibe_client_default.cfg
iindex ffa9d29..4c9b283 100644
---- a/game/openvibe.games/cfg/openvibe_client_default.cfg
++++ b/game/openvibe.games/cfg/openvibe_client_default.cfg
@@@ -9,4 +9,11 @@ con_enable 0
bind "`" "ov_console"
bind "F10" "ov_console"
bind "F1" "ov_menu_main"
++
++// REQUIRED for local dev: Source routes 127.0.0.1 through its in-process
++// listen-server loopback queue by default, so connects to a separate local
++// DEDICATED server never leave the process — "Connection failed after 4
++// retries" while the server sees nothing. Real sockets fix it.
++net_usesocketsforloopback 1
++
echo "OpenVibe: stock console disabled; ` and F10 toggle the OpenVibe console"
ddiff --git a/game/openvibe.games/cfg/openvibe_common.cfg b/game/openvibe.games/cfg/openvibe_common.cfg
iindex f461fb9..7c3cee3 100644
---- a/game/openvibe.games/cfg/openvibe_common.cfg
++++ b/game/openvibe.games/cfg/openvibe_common.cfg
@@@ -1,6 +1,14 @@
// Shared local development defaults for OpenVibe: Source.
--sv_lan 1
--sv_master_share_game_socket 1
++//
++// sv_lan MUST stay 0 on this engine branch: with sv_lan 1 the Steam
++// gameserver never logs on ("Initializing Steam libraries for LAN server")
++// and the engine then drops ALL unconnected packets — no A2S query replies,
++// no connect challenges, so clients sit at "Connecting to server..." and die
++// with "Connection failed after 4 retries". With sv_lan 0 the anonymous
++// logon completes ("Connection to Steam servers successful") and local
++// connects work. -nomaster still keeps dev shards off the public browser.
++sv_lan 0
++sv_master_share_game_socket 0
sv_cheats 0
sv_pure 0
developer 1
ddiff --git a/game/openvibe.games/cfg/openvibe_proton_stability.cfg b/game/openvibe.games/cfg/openvibe_proton_stability.cfg
iindex d70452b..00a7b27 100644
---- a/game/openvibe.games/cfg/openvibe_proton_stability.cfg
++++ b/game/openvibe.games/cfg/openvibe_proton_stability.cfg
@@@ -23,10 +23,12 @@ snd_mute_losefocus 0
mat_dxlevel 90
r_3dsky 0
--// Local dev listen server: sv_lan 1 stops master-server heartbeats, which
--// silences the repeated "Your server is out of date. Please update and
--// restart." (MasterRequestRestart) spam from the older engine build.
--sv_lan 1
++// NOTE: never set sv_lan 1 here. Client-side LAN mode skips the Steam auth
++// session, and this engine build then silently fails to emit connect packets:
++// every join dies with "Connection failed after 4 retries" while the server
++// sees nothing. The out-of-date heartbeat spam it used to silence is gone now
++// that the dedicated servers run -nomaster.
++sv_lan 0
// Auto-open the custom HTML menu so it covers the stock HL2 main menu (which
// freezes) — this is the menu override. The menu only joins a server on an
ddiff --git a/sdk/openvibe/client/hl2mp/openvibe_js_client.cpp b/sdk/openvibe/client/hl2mp/openvibe_js_client.cpp
iindex 33b12d0..3b22f02 100644
---- a/sdk/openvibe/client/hl2mp/openvibe_js_client.cpp
++++ b/sdk/openvibe/client/hl2mp/openvibe_js_client.cpp
@@@ -171,8 +171,41 @@ void OpenVibeJS_Client_Shutdown()
g_bClientBridgeStarted = false;
}
++// Implemented in vgui_openvibe_menu.cpp (console spew ring + menu override).
++extern bool OpenVibe_DrainConsoleLine( int64 *pnCursor, char *pszOut, int nOutLen );
++extern void OpenVibe_MenuKeepAlive();
++
++// Escape a console line for embedding in a JSON string value.
++static void OVClient_JSONEscape( char *pszOut, int nOutLen, const char *pszIn )
++{
++ int w = 0;
++ for ( const char *p = pszIn; *p && w < nOutLen - 2; ++p )
++ {
++ unsigned char c = (unsigned char)*p;
++ if ( c == '"' || c == '\\' )
++ {
++ if ( w >= nOutLen - 3 ) break;
++ pszOut[w++] = '\\';
++ pszOut[w++] = c;
++ }
++ else if ( c < 0x20 )
++ {
++ pszOut[w++] = ' ';
++ }
++ else
++ {
++ pszOut[w++] = c;
++ }
++ }
++ pszOut[w] = '\0';
++}
++
void OpenVibeJS_Client_Think()
{
++ // Keep the OpenVibe HTML menu covering the stock GameUI whenever we are
++ // out of a level (runs regardless of runtime connectivity).
++ OpenVibe_MenuKeepAlive();
++
g_ClientIPC.Poll();
if ( !g_ClientIPC.IsConnected() )
@@@ -187,6 +220,21 @@ void OpenVibeJS_Client_Think()
s_flNextThink = gpGlobals->curtime + 0.1f;
g_ClientIPC.SendLine( "{\"t\":\"think\"}" );
}
++
++ // Mirror engine console lines into the runtime's log stream so the GUI
++ // console shows engine output in every host (launcher included).
++ static int64 s_nConsoleCursor = 0;
++ char szLine[480];
++ int nSent = 0;
++ while ( nSent < 10 && OpenVibe_DrainConsoleLine( &s_nConsoleCursor, szLine, sizeof( szLine ) ) )
++ {
++ char szEsc[960];
++ OVClient_JSONEscape( szEsc, sizeof( szEsc ), szLine );
++ char szMsg[1100];
++ Q_snprintf( szMsg, sizeof( szMsg ), "{\"t\":\"conline\",\"line\":\"%s\"}", szEsc );
++ g_ClientIPC.SendLine( szMsg );
++ ++nSent;
++ }
}
static void OV_ClientJSStatus_f()
@@@ -273,14 +321,18 @@ static void OV_JSOpenScriptCl_f( const CCommand &args )
static ConCommand js_openscript_cl( "js_openscript_cl", OV_JSOpenScriptCl_f,
"Run a script file in the client realm: js_openscript_cl <path relative to js/>. Gated by sv_allowcsjs.", FCVAR_CLIENTDLL );
--// Drives the bridge: hook OVNet, connect on level enter, poll each frame.
++// Drives the bridge: hook OVNet, connect at boot AND on level enter, poll
++// each frame. Connecting at boot means the GUI console's client realm works
++// from the main menu, not just in-game.
class COpenVibeClientJSSystem : public CAutoGameSystemPerFrame
{
public:
COpenVibeClientJSSystem() : CAutoGameSystemPerFrame( "COpenVibeClientJSSystem" ) {}
bool Init() OVERRIDE { usermessages->HookMessage( "OVNet", __MsgFunc_OVNet ); return true; }
++ void PostInit() OVERRIDE { OpenVibeJS_Client_Init(); }
void LevelInitPostEntity() OVERRIDE { OpenVibeJS_Client_Shutdown(); OpenVibeJS_Client_Init(); }
-- void LevelShutdownPostEntity() OVERRIDE { OpenVibeJS_Client_Shutdown(); }
++ // Reconnect (rather than just close) so the bridge stays live at the menu.
++ void LevelShutdownPostEntity() OVERRIDE { OpenVibeJS_Client_Shutdown(); OpenVibeJS_Client_Init(); }
void Update( float ) OVERRIDE { OpenVibeJS_Client_Think(); }
};
static COpenVibeClientJSSystem g_OpenVibeClientJSSystem;
ddiff --git a/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp b/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp
iindex daa1209..61ddef5 100644
---- a/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp
++++ b/sdk/openvibe/client/hl2mp/vgui_openvibe_menu.cpp
@@@ -84,9 +84,25 @@ static bool OV_IsSafeMode( const char *pszMode )
!Q_stricmp( pszMode, "traitortown" ) );
}
--// openvibe://cmd allowlist: the first token decides. ov_* commands (which
--// includes ov_npm and all OpenVibe dev commands) plus the GModJS script
--// commands and a few stock engine commands the GUI console needs.
++// The OpenVibe HTML console replaces the stock Source console outright. When
++// the panel is showing OUR OWN local UI (127.0.0.1/localhost origins) the
++// bridge executes any console line, exactly like the real console. Remote
++// origins (openvibe.games) stay restricted to the allowlist below.
++static ConVar ov_console_unrestricted(
++ "ov_console_unrestricted",
++ "1",
++ FCVAR_CLIENTDLL | FCVAR_ARCHIVE,
++ "Allow the local OpenVibe HTML console to run any engine command (remote pages stay allowlisted)." );
++
++static bool OV_IsLocalUIOrigin( const char *pszUrl )
++{
++ return OV_URLStartsWith( pszUrl, "http://127.0.0.1:" ) ||
++ OV_URLStartsWith( pszUrl, "http://localhost:" );
++}
++
++// openvibe://cmd allowlist for NON-local origins: the first token decides.
++// ov_* commands (which includes ov_npm and all OpenVibe dev commands) plus the
++// GModJS script commands and a few stock engine commands the GUI console needs.
static bool OV_IsAllowedBridgeCommand( const char *pszCommand )
{
if ( !pszCommand || !pszCommand[0] )
@@@ -285,6 +301,7 @@ public:
void Open( const char *pszURL = NULL )
{
const char *pszTarget = ( pszURL && pszURL[0] ) ? pszURL : ov_menu_url.GetString();
++ Q_strncpy( m_szCurrentURL, pszTarget, sizeof( m_szCurrentURL ) );
MakeFullScreen();
SetVisible( true );
MoveToFront();
@@@ -345,6 +362,9 @@ public:
return false;
}
++ // Track the page origin: the bridge grants full console powers only to
++ // the local UI (see openvibe://cmd handling).
++ Q_strncpy( m_szCurrentURL, pszURL, sizeof( m_szCurrentURL ) );
return true;
}
@@@ -408,17 +428,30 @@ private:
if ( OV_URLStartsWith( pszURL, "openvibe://cmd" ) )
{
-- // GUI console -> engine command, allowlisted by first token.
++ // GUI console -> engine command. This is the user's own console
++ // replacement: pages served from the local UI run any command line
++ // (like the stock console, incl. map/changelevel/cheats/binds);
++ // remote pages stay restricted to the first-token allowlist.
char szCmd[1024];
if ( !OV_ReadQueryValue( pszURL, "c", szCmd, sizeof( szCmd ) ) )
return;
-- if ( !OV_IsAllowedBridgeCommand( szCmd ) )
++ // Newlines never pass (one console line per bridge action).
++ for ( char *p = szCmd; *p; ++p )
++ {
++ if ( *p == '\n' || *p == '\r' )
++ *p = ' ';
++ }
++
++ const bool bLocalConsole = ov_console_unrestricted.GetBool() && OV_IsLocalUIOrigin( m_szCurrentURL );
++ if ( !bLocalConsole && !OV_IsAllowedBridgeCommand( szCmd ) )
{
-- Warning( "[OV HTML] blocked bridge command: %s\n", szCmd );
++ Warning( "[OV HTML] blocked bridge command from non-local page: %s\n", szCmd );
return;
}
++ // Echo like the stock console so the command shows in the tap.
++ Msg( "] %s\n", szCmd );
engine->ClientCmd_Unrestricted( VarArgs( "%s\n", szCmd ) );
return;
}
@@@ -495,6 +528,7 @@ private:
}
HTML *m_pHTML;
++ char m_szCurrentURL[512] = {};
};
static COpenVibeHTMLPanel *s_pOpenVibeMenu = NULL;
@@@ -615,6 +649,46 @@ static void OV_ConsoleTapThink()
s_pOpenVibeMenu->RunJS( szScript );
s_bInConsoleTap = false;
}
++
++// Second consumer of the console ring: the client JS bridge drains lines and
++// forwards them to the Node runtime, whose SSE log stream feeds the GUI
++// console in EVERY host (launcher included). Returns false when caught up.
++bool OpenVibe_DrainConsoleLine( int64 *pnCursor, char *pszOut, int nOutLen )
++{
++ if ( !pnCursor || !pszOut || nOutLen <= 0 )
++ return false;
++
++ if ( *pnCursor >= s_nRingWritten )
++ return false;
++
++ if ( s_nRingWritten - *pnCursor > OV_CONSOLE_RING_LINES )
++ *pnCursor = s_nRingWritten - OV_CONSOLE_RING_LINES;
++
++ Q_strncpy( pszOut, s_ConsoleRing[*pnCursor % OV_CONSOLE_RING_LINES], nOutLen );
++ ++( *pnCursor );
++ return true;
++}
++
++// The OpenVibe HTML menu fully replaces the stock GameUI menu: whenever we are
++// not in a level and the panel got hidden (e.g. the engine popped a
++// "Disconnected" dialog over the stock menu), bring ours back.
++void OpenVibe_MenuKeepAlive()
++{
++ if ( !ov_menu_auto_open.GetBool() )
++ return;
++
++ static float s_flNextCheck = 0.0f;
++ const float flNow = Plat_FloatTime();
++ if ( flNow < s_flNextCheck )
++ return;
++ s_flNextCheck = flNow + 2.0f;
++
++ if ( engine->IsInGame() )
++ return;
++
++ if ( !s_pOpenVibeMenu || !s_pOpenVibeMenu->IsVisible() )
++ OV_GetHTMLMenu()->Open();
++}
// OPENVIBE_CONSOLE_SPEW_TAP_END
static void OV_OpenMenu_f( const CCommand &args )
ddiff --git a/tools/run-client-auto.sh b/tools/run-client-auto.sh
iindex 1757dac..4fcd757 100755
---- a/tools/run-client-auto.sh
++++ b/tools/run-client-auto.sh
@@@ -29,7 +29,9 @@ case "$MODE" in
echo "Build Windows DLLs first, or use OPENVIBE_CLIENT_MODE=proton-fallback to launch without client DLL." >&2
exit 1
fi
-- exec "$ROOT/tools/run-client-proton.sh" "${@:-}"
++ # hl2_win64.exe is the engine our x64 DLLs load into (bin/x64); the 32-bit
++ # hl2.exe silently ignores them and boots the stock client.
++ exec "$ROOT/tools/run-client-proton-x64.sh" "${@:-}"
;;
proton-fallback)
echo "WARNING: Proton fallback may launch, but without game/openvibe.games/bin/client.dll the in-game ov_* client commands/menu will not exist." >&2
ddiff --git a/tools/run-client-proton-x64.sh b/tools/run-client-proton-x64.sh
iindex 19ad174..a7665f2 100755
---- a/tools/run-client-proton-x64.sh
++++ b/tools/run-client-proton-x64.sh
@@@ -56,7 +56,7 @@ echo " Proton: $GE_PROTON"
echo " HL2: $HL2_EXE"
[ -n "$CONNECT_ARGS" ] && echo " Connect: $CONNECT_ARGS"
--if [ -f "$CLIENT_DLL" ] && command -v strings >/dev/null 2>&1 && strings -a "$CLIENT_DLL" | grep -Fq "ov_join"; then
++if [ -f "$CLIENT_DLL" ] && command -v strings >/dev/null 2>&1 && [ "$(strings -a "$CLIENT_DLL" | grep -Fc "ov_join" || true)" -gt 0 ]; then
echo " Client DLL: patched OpenVibe client.dll detected"
elif [ -f "$CLIENT_DLL" ]; then
echo " WARNING: client.dll exists, but ov_join string was not found. It may be stock/old." >&2
@@@ -91,13 +91,18 @@ fi
# Single canonical log via -condebug (writes game/openvibe.games/console.log).
# Do NOT also set +con_logfile: it hijacks the console log mid-startup, so the
# two split and neither is complete. -condebug alone keeps one full log.
++# NOTE: no -insecure here — an insecure client is rejected by VAC-secured
++# servers with "You are in insecure mode. You must restart before you can
++# connect to secure servers." Local dev servers run -insecure server-side
++# instead (see run-server.sh).
exec "$GE_PROTON/proton" waitforexitandrun \
"$HL2_EXE" \
-game "$GAME_DIR" \
$CONSOLE_ARGS -dev -condebug -novid -sw -w 1280 -h 720 \
-port 27115 -clientport 27105 \
-- -nojoy -insecure -nohltv \
++ -nojoy -nohltv \
+developer 1 \
++ +net_usesocketsforloopback 1 \
+exec openvibe_proton_stability.cfg \
+exec openvibe_proton_client.cfg \
+exec openvibe_client_default.cfg \
ddiff --git a/tools/run-client-proton.sh b/tools/run-client-proton.sh
iindex 9b341c6..c62f2df 100755
---- a/tools/run-client-proton.sh
++++ b/tools/run-client-proton.sh
@@@ -32,7 +32,7 @@ echo " Proton: $GE_PROTON"
echo " HL2: $HL2_EXE"
[ -n "$CONNECT_ARGS" ] && echo " Connect: $CONNECT_ARGS"
--if [ -f "$CLIENT_DLL" ] && command -v strings >/dev/null 2>&1 && strings -a "$CLIENT_DLL" | grep -Fq "ov_join"; then
++if [ -f "$CLIENT_DLL" ] && command -v strings >/dev/null 2>&1 && [ "$(strings -a "$CLIENT_DLL" | grep -Fc "ov_join" || true)" -gt 0 ]; then
echo " Client DLL: patched OpenVibe client.dll detected"
elif [ -f "$CLIENT_DLL" ]; then
echo " WARNING: client.dll exists, but ov_join string was not found. It may be stock/old." >&2
ddiff --git a/tools/run-server.sh b/tools/run-server.sh
iindex f25c498..90741f8 100755
---- a/tools/run-server.sh
++++ b/tools/run-server.sh
@@@ -69,6 +69,13 @@ server_cmd=(
-steamport "$STEAMPORT"
)
++# Local dev shards run without VAC (a VAC-secured server rejects clients that
++# were ever started -insecure, and dev iterating on DLLs trips VAC anyway).
++# Set OPENVIBE_SRCDS_SECURE=1 for a secured public shard.
++if [[ "${OPENVIBE_SRCDS_SECURE:-0}" != "1" ]]; then
++ server_cmd+=(-insecure)
++fi
++
if [[ "${OPENVIBE_SRCDS_MASTER:-0}" != "1" ]]; then
server_cmd+=(-nomaster)
fi
@@@ -77,7 +84,10 @@ server_cmd+=(
+clientport "$CLIENTPORT"
+maxplayers "$MAXPLAYERS"
+tv_port "$TVPORT"
-- +sv_master_share_game_socket 1
++ # share_game_socket=1 hands unconnected packets (A2S queries + connect
++ # challenges) to the Steam GS layer, which stays dead for anonymous local
++ # shards — clients then fail with "Connection failed after 4 retries".
++ +sv_master_share_game_socket 0
+exec "$CFG"
)