openvibers/source.openvibe.games Commits
Add Sandbox gamemode with GMod-style Q menu (JS engine)
Changes
@@@ -7,3 +7,9 @@ alias ovp_join_prophunt "connect 127.0.0.1:27016"alias ovp_join_deathrun "connect 127.0.0.1:27017"alias ovp_join_fortwars "connect 127.0.0.1:27018"alias ovp_join_traitortown "connect 127.0.0.1:27019"++alias ovp_join_sandbox "connect 127.0.0.1:27020"++++// Sandbox Q menu (GMod-style spawn menu). Q sends the !q chat command, which the++// server-side JS command registry intercepts and shows the spawn menu.++bind q "say !q"++echo "OpenVibe: Q bound to sandbox spawn menu (say !q)"nnew file mode 100644@@@ -0,0 +1,13 @@++exec openvibe_common.cfg++++hostname "OpenVibe: Source | Sandbox Local"++maxplayers 24++++// Sandbox uses the embedded JS "sandbox" gamemode (js/gamemodes/sandbox/).++// Unlike the other dev cfgs, ov_mode is set (not commented) so LoadGamemode++// pulls js/gamemodes/sandbox/server.js instead of defaulting to hub.++ov_mode "sandbox"++++// No round timer in sandbox.++mp_timelimit 0++mp_fraglimit 0nnew file mode 100644@@@ -0,0 +1,6 @@++// OpenVibe Sandbox — client realm.++// The spawn menu is served by the server realm (server.js) and shown via chat.++// The actual "press Q" binding is installed from the client cfg++// (openvibe_proton_client.cfg: bind q "say !q"), which routes to the server's++// !q command through the PlayerSay hook. This file just marks the realm loaded.++console.log("OpenVibe Sandbox client realm loaded — press Q for the spawn menu.");nnew file mode 100644@@@ -0,0 +1,10 @@++{++ "id": "sandbox",++ "name": "OpenVibe Sandbox",++ "base": "base",++ "entry": {++ "shared": "shared.js",++ "server": "server.js",++ "client": "client.js"++ }++}nnew file mode 100644@@@ -0,0 +1,131 @@++// OpenVibe Sandbox — server realm.++//++// Free-build sandbox mode with a GMod-style Q menu (spawn menu) implemented++// entirely in the JS engine. There are no rounds: props can be spawned at any++// time. The Q menu is a chat/console-driven spawn menu because the JS runtime++// runs server-side; a keybind (bind q "ov_qmenu") forwards to the server which++// prints the menu to the requesting player.++(function () {++ // The shared realm isn't auto-loaded by the C++ core loader (only++ // base/server.js + <mode>/server.js are), so pull the catalog in defensively:++ // if OVSandbox isn't defined yet, define a minimal fallback matching shared.js.++ const catalog = (globalThis.OVSandbox && globalThis.OVSandbox.SPAWN_CATALOG) || [++ {++ category: "Build Props",++ items: [++ { id: "crate", label: "Wooden Crate" },++ { id: "barrel", label: "Oil Drum" },++ { id: "pallet", label: "Wood Pallet" },++ { id: "fence", label: "Wood Fence" },++ { id: "sheet", label: "Mattress" }++ ]++ }++ ];++++ const spawnable = Object.create(null);++ const numbered = [];++ catalog.forEach(function (cat) {++ cat.items.forEach(function (item) {++ spawnable[item.id] = item.label;++ numbered.push(item);++ });++ });++++ function ensureBuildEnabled() {++ // ov_fortwars_spawn is gated behind this convar; sandbox keeps it on always.++ OV.serverCommand("ov_fortwars_build_enabled 1");++ }++++ function spawnProp(ply, id) {++ const key = String(id || "").toLowerCase();++ if (!spawnable[key]) {++ if (ply) ply.chat(`Can't spawn "${id}". Type !q for the spawn menu.`);++ return false;++ }++ if (!ply) return false;++ ensureBuildEnabled();++ ply.runCommand(`ov_fortwars_spawn ${key}`);++ ply.chat(`Spawned: ${spawnable[key]}`);++ return true;++ }++++ // Render the Q menu to a single player as chat lines. Numbered so a player can++ // spawn by number (!1) or by id (!spawn crate).++ function openQMenu(ply) {++ if (!ply) return;++ ply.chat("=== OpenVibe Q Menu — Spawn ===");++ let n = 0;++ catalog.forEach(function (cat) {++ ply.chat(`- ${cat.category} -`);++ cat.items.forEach(function (item) {++ n += 1;++ ply.chat(` ${n}. ${item.label} (!spawn ${item.id} or !${n})`);++ });++ });++ ply.chat("Spawn appears where you're looking.");++ }++++ function registerCommands() {++ if (!globalThis.command) return;++++ // Q menu open — chat (!q / !qmenu) and console (ov_qmenu via ConsoleCommand hook).++ command.add("q", "Open the sandbox spawn (Q) menu", function ({ ply }) {++ openQMenu(ply);++ return false;++ });++ command.add("qmenu", "Open the sandbox spawn (Q) menu", function ({ ply }) {++ openQMenu(ply);++ return false;++ });++++ // Spawn by id: !spawn crate++ command.add("spawn", "Spawn a prop: !spawn <crate|barrel|pallet|fence|sheet>", function ({ ply, args }) {++ spawnProp(ply, args[0] || "crate");++ return false;++ });++++ // Spawn by menu number: !1 .. !N++ numbered.forEach(function (item, idx) {++ const num = String(idx + 1);++ command.add(num, `Spawn ${item.label}`, function ({ ply }) {++ spawnProp(ply, item.id);++ return false;++ });++ });++ }++++ const GM = {++ mode: "sandbox",++ name: "OpenVibe Sandbox",++++ Initialize() {++ OV.log("Sandbox Initialize fired");++ registerCommands();++ ensureBuildEnabled();++ },++++ MapInitialize(mapName) {++ OV.log(`Sandbox MapInitialize: ${mapName}`);++ ensureBuildEnabled();++ // Sandbox has no rounds — deliberately do NOT call scheduleRoundStart().++ },++++ // No rounds in sandbox: neutralize the base round scheduler entirely.++ scheduleRoundStart() {},++ startRound() {},++ endRound() {},++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe Sandbox! Press Q (or type !q) for the spawn menu.");++ },++++ PlayerSpawn(ply) {++ ensureBuildEnabled();++ if (ply) ply.chat("Sandbox: !q spawn menu · !spawn crate");++ },++++ Think() {}++ };++++ gamemode.set(GM);++})();nnew file mode 100644@@@ -0,0 +1,40 @@++// OpenVibe Sandbox — shared realm.++// The spawn catalog lives here so both the server (which does the actual++// spawning) and the client (which can render the Q menu) agree on the exact++// same categories and prop ids without duplicating the list.++(function () {++ // Every prop id here must be allowlisted by the C++ ov_fortwars_spawn command,++ // which is what actually creates the prop_physics entity server-side.++ const SPAWN_CATALOG = [++ {++ category: "Build Props",++ items: [++ { id: "crate", label: "Wooden Crate" },++ { id: "barrel", label: "Oil Drum" },++ { id: "pallet", label: "Wood Pallet" },++ { id: "fence", label: "Wood Fence" },++ { id: "sheet", label: "Mattress" }++ ]++ }++ ];++++ // Flatten to a quick id -> label lookup for validation / menu numbering.++ const byId = Object.create(null);++ const flat = [];++ SPAWN_CATALOG.forEach(function (cat) {++ cat.items.forEach(function (item) {++ byId[item.id] = item;++ flat.push(item);++ });++ });++++ globalThis.OVSandbox = {++ SPAWN_CATALOG: SPAWN_CATALOG,++ spawnItems: flat,++ isSpawnable: function (id) { return !!byId[String(id || "").toLowerCase()]; },++ labelFor: function (id) {++ const item = byId[String(id || "").toLowerCase()];++ return item ? item.label : id;++ }++ };++})();nnew file mode 100755@@@ -0,0 +1,68 @@++#!/bin/bash++# OpenVibe: Source - Game Client Launcher via GE-Proton10-34++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++STEAM_PATH="${OPENVIBE_STEAM_PATH:-/mnt/6tb/ssd_offload/home/workstation/.steam/debian-installation}"++GE_PROTON="${OPENVIBE_GE_PROTON:-$STEAM_PATH/compatibilitytools.d/GE-Proton10-34}"++STEAM_COMPAT_DATA="${OPENVIBE_STEAM_COMPAT_DATA:-$STEAM_PATH/steamapps/compatdata/243750}"++HL2_EXE="${OPENVIBE_HL2_EXE:-/mnt/data-f/SteamLibrary/steamapps/common/Source SDK Base 2013 Multiplayer/hl2_win64.exe}"++GAME_DIR="${OPENVIBE_GAME_DIR:-$ROOT/game/openvibe.games}"++CLIENT_DLL="$GAME_DIR/bin/x64/client.dll"++SERVER_DLL="$GAME_DIR/bin/x64/server.dll"++++if [ ! -f "$HL2_EXE" ]; then++ echo "ERROR: hl2.exe not found at $HL2_EXE" >&2++ exit 1++fi++++if [ ! -d "$GE_PROTON" ]; then++ echo "ERROR: GE-Proton not found at $GE_PROTON" >&2++ exit 1++fi++++CONNECT_ARGS=""++if [ "${1:-}" != "" ] && [ "${2:-}" != "" ]; then++ CONNECT_ARGS="+connect $1:$2"++fi++++echo "Launching OpenVibe: Source via Proton..."++echo " Game: $GAME_DIR"++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++ 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++else++ echo " WARNING: client.dll missing. Proton will not have OpenVibe client commands." >&2++fi++++if [ ! -f "$SERVER_DLL" ]; then++ echo " WARNING: server.dll missing for Windows listen/server use." >&2++fi++++mkdir -p "$STEAM_COMPAT_DATA"++++export DISPLAY="${DISPLAY:-:0}"++export STEAM_COMPAT_CLIENT_INSTALL_PATH="$STEAM_PATH"++export STEAM_COMPAT_DATA_PATH="$STEAM_COMPAT_DATA"++export SteamAppId="${SteamAppId:-243750}"++export PROTON_LOG="${OPENVIBE_PROTON_LOG:-${PROTON_LOG:-1}}"++++# DXVK_ASYNC=1 races async shader pipeline compilation against DXVK swapchain++# teardown on first launch; hl2.exe intermittently dies right after the first++# frame with no error trace. Default off for reliability.++export DXVK_ASYNC="${DXVK_ASYNC:-0}"++++exec "$GE_PROTON/proton" waitforexitandrun \++ "$HL2_EXE" \++ -game "$GAME_DIR" \++ -console -dev -condebug -novid -sw -w 1280 -h 720 \++ -port 27115 -clientport 27105 \++ +developer 1 \++ +con_logfile openvibe_proton_console.log \++ +exec openvibe_proton_client.cfg \++ $CONNECT_ARGSnnew file mode 100755@@@ -0,0 +1,81 @@++#!/usr/bin/env bash++set -euo pipefail++++# OpenVibe Sandbox dev server (free-build + JS Q menu).++#++# Standalone launcher (does not use run-server.sh's fixed map_input) because the++# sandbox needs to force the JS gamemode to "sandbox" AFTER the map is up:++# loading the ov_hub map auto-execs cfg/ov_hub.cfg -> openvibe_hub.cfg, and the++# JS runtime latches ov_mode on first tick. ov_js_reload re-reads ov_mode and++# reloads the gamemode, so we set ov_mode + reload once the server is live.++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++SRCDS="${OPENVIBE_SRCDS:-$HOME/srcds/tf2}"++MOD="$ROOT/game/openvibe.games"++PORT="${OPENVIBE_SANDBOX_PORT:-27020}"++MAP="${OPENVIBE_SANDBOX_MAP:-ov_hub}"++MAXPLAYERS="${OPENVIBE_SANDBOX_MAXPLAYERS:-24}"++BIND_IP="${OPENVIBE_BIND_IP:-127.0.0.1}"++MAP_DELAY="${OPENVIBE_SRCDS_MAP_DELAY:-6}"++RELOAD_DELAY="${OPENVIBE_SANDBOX_RELOAD_DELAY:-8}"++++if [[ ! -x "$SRCDS/srcds_linux64" ]]; then++ echo "Missing 64-bit SRCDS at $SRCDS/srcds_linux64" >&2++ exit 1++fi++if [[ ! -f "$MOD/maps/$MAP.bsp" ]]; then++ echo "Missing map $MOD/maps/$MAP.bsp" >&2++ exit 1++fi++++cd "$SRCDS"++printf '243750\n' > "$SRCDS/steam_appid.txt"++++COMPAT_LIB_DIR="${OPENVIBE_SRCDS_COMPAT_LIB_DIR:-/tmp/openvibe-srcds-compat-$USER}"++if [[ ! -e /lib/i386-linux-gnu/libtinfo.so.5 && -e /lib/i386-linux-gnu/libtinfo.so.6 ]]; then++ mkdir -p "$COMPAT_LIB_DIR"++ ln -sfn /lib/i386-linux-gnu/libtinfo.so.6 "$COMPAT_LIB_DIR/libtinfo.so.5"++fi++if [[ ! -e /lib/i386-linux-gnu/libncurses.so.5 && -e /lib/i386-linux-gnu/libncurses.so.6 ]]; then++ mkdir -p "$COMPAT_LIB_DIR"++ ln -sfn /lib/i386-linux-gnu/libncurses.so.6 "$COMPAT_LIB_DIR/libncurses.so.5"++fi++++export LD_LIBRARY_PATH="$COMPAT_LIB_DIR:$MOD/bin/linux64:.:$SRCDS/bin/linux64:$SRCDS/bin:${LD_LIBRARY_PATH:-}"++++server_cmd=(++ ./srcds_linux64++ -game "$MOD"++ -console++ -usercon++ -ip "$BIND_IP"++ -port "$PORT"++ +clientport "$((PORT + 1000))"++ +maxplayers "$MAXPLAYERS"++ +tv_port "$((PORT + 2000))"++ +exec openvibe_sandbox.cfg++)++++printf -v server_command '%q ' "${server_cmd[@]}"++++console_input() {++ sleep "$MAP_DELAY"++ printf 'map %s\n' "$MAP"++ # After the map is live and the JS runtime has started, force sandbox mode.++ sleep "$RELOAD_DELAY"++ printf 'ov_mode sandbox\n'++ printf 'ov_js_reload\n'++ while sleep 3600; do :; done++}++++filter_output() {++ sed -u \++ -e '/WARNING: Failed to load 32-bit libtinfo\.so\.5 or libncurses\.so\.5\./d' \++ -e '/Please install (lib32tinfo5 \/ ncurses-libs\.i686 \/ equivalent) to enable readline\./d'++}++++if command -v script >/dev/null 2>&1; then++ console_input | script -qfec "$server_command" /dev/null | filter_output++else++ console_input | "${server_cmd[@]}" | filter_output++fi
@@ -7,3 +7,9 @@ alias ovp_join_prophunt "connect 127.0.0.1:27016"
@@ -7,3 +7,9 @@ alias ovp_join_prophunt "connect 127.0.0.1:27016"
alias ovp_join_deathrun "connect 127.0.0.1:27017"
alias ovp_join_deathrun "connect 127.0.0.1:27017"
alias ovp_join_fortwars "connect 127.0.0.1:27018"
alias ovp_join_fortwars "connect 127.0.0.1:27018"
alias ovp_join_traitortown "connect 127.0.0.1:27019"
alias ovp_join_traitortown "connect 127.0.0.1:27019"
+alias ovp_join_sandbox "connect 127.0.0.1:27020"
+
+// Sandbox Q menu (GMod-style spawn menu). Q sends the !q chat command, which the
+// server-side JS command registry intercepts and shows the spawn menu.
+bind q "say !q"
+echo "OpenVibe: Q bound to sandbox spawn menu (say !q)"
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+exec openvibe_common.cfg
+
+hostname "OpenVibe: Source | Sandbox Local"
+maxplayers 24
+
+// Sandbox uses the embedded JS "sandbox" gamemode (js/gamemodes/sandbox/).
+// Unlike the other dev cfgs, ov_mode is set (not commented) so LoadGamemode
+// pulls js/gamemodes/sandbox/server.js instead of defaulting to hub.
+ov_mode "sandbox"
+
+// No round timer in sandbox.
+mp_timelimit 0
+mp_fraglimit 0
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+// OpenVibe Sandbox — client realm.
+// The spawn menu is served by the server realm (server.js) and shown via chat.
+// The actual "press Q" binding is installed from the client cfg
+// (openvibe_proton_client.cfg: bind q "say !q"), which routes to the server's
+// !q command through the PlayerSay hook. This file just marks the realm loaded.
+console.log("OpenVibe Sandbox client realm loaded — press Q for the spawn menu.");
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+{
+ "id": "sandbox",
+ "name": "OpenVibe Sandbox",
+ "base": "base",
+ "entry": {
+ "shared": "shared.js",
+ "server": "server.js",
+ "client": "client.js"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,131 @@
@@ -0,0 +1,131 @@
+// OpenVibe Sandbox — server realm.
+//
+// Free-build sandbox mode with a GMod-style Q menu (spawn menu) implemented
+// entirely in the JS engine. There are no rounds: props can be spawned at any
+// time. The Q menu is a chat/console-driven spawn menu because the JS runtime
+// runs server-side; a keybind (bind q "ov_qmenu") forwards to the server which
+// prints the menu to the requesting player.
+(function () {
+ // The shared realm isn't auto-loaded by the C++ core loader (only
+ // base/server.js + <mode>/server.js are), so pull the catalog in defensively:
+ // if OVSandbox isn't defined yet, define a minimal fallback matching shared.js.
+ const catalog = (globalThis.OVSandbox && globalThis.OVSandbox.SPAWN_CATALOG) || [
+ {
+ category: "Build Props",
+ items: [
+ { id: "crate", label: "Wooden Crate" },
+ { id: "barrel", label: "Oil Drum" },
+ { id: "pallet", label: "Wood Pallet" },
+ { id: "fence", label: "Wood Fence" },
+ { id: "sheet", label: "Mattress" }
+ ]
+ }
+ ];
+
+ const spawnable = Object.create(null);
+ const numbered = [];
+ catalog.forEach(function (cat) {
+ cat.items.forEach(function (item) {
+ spawnable[item.id] = item.label;
+ numbered.push(item);
+ });
+ });
+
+ function ensureBuildEnabled() {
+ // ov_fortwars_spawn is gated behind this convar; sandbox keeps it on always.
+ OV.serverCommand("ov_fortwars_build_enabled 1");
+ }
+
+ function spawnProp(ply, id) {
+ const key = String(id || "").toLowerCase();
+ if (!spawnable[key]) {
+ if (ply) ply.chat(`Can't spawn "${id}". Type !q for the spawn menu.`);
+ return false;
+ }
+ if (!ply) return false;
+ ensureBuildEnabled();
+ ply.runCommand(`ov_fortwars_spawn ${key}`);
+ ply.chat(`Spawned: ${spawnable[key]}`);
+ return true;
+ }
+
+ // Render the Q menu to a single player as chat lines. Numbered so a player can
+ // spawn by number (!1) or by id (!spawn crate).
+ function openQMenu(ply) {
+ if (!ply) return;
+ ply.chat("=== OpenVibe Q Menu — Spawn ===");
+ let n = 0;
+ catalog.forEach(function (cat) {
+ ply.chat(`- ${cat.category} -`);
+ cat.items.forEach(function (item) {
+ n += 1;
+ ply.chat(` ${n}. ${item.label} (!spawn ${item.id} or !${n})`);
+ });
+ });
+ ply.chat("Spawn appears where you're looking.");
+ }
+
+ function registerCommands() {
+ if (!globalThis.command) return;
+
+ // Q menu open — chat (!q / !qmenu) and console (ov_qmenu via ConsoleCommand hook).
+ command.add("q", "Open the sandbox spawn (Q) menu", function ({ ply }) {
+ openQMenu(ply);
+ return false;
+ });
+ command.add("qmenu", "Open the sandbox spawn (Q) menu", function ({ ply }) {
+ openQMenu(ply);
+ return false;
+ });
+
+ // Spawn by id: !spawn crate
+ command.add("spawn", "Spawn a prop: !spawn <crate|barrel|pallet|fence|sheet>", function ({ ply, args }) {
+ spawnProp(ply, args[0] || "crate");
+ return false;
+ });
+
+ // Spawn by menu number: !1 .. !N
+ numbered.forEach(function (item, idx) {
+ const num = String(idx + 1);
+ command.add(num, `Spawn ${item.label}`, function ({ ply }) {
+ spawnProp(ply, item.id);
+ return false;
+ });
+ });
+ }
+
+ const GM = {
+ mode: "sandbox",
+ name: "OpenVibe Sandbox",
+
+ Initialize() {
+ OV.log("Sandbox Initialize fired");
+ registerCommands();
+ ensureBuildEnabled();
+ },
+
+ MapInitialize(mapName) {
+ OV.log(`Sandbox MapInitialize: ${mapName}`);
+ ensureBuildEnabled();
+ // Sandbox has no rounds — deliberately do NOT call scheduleRoundStart().
+ },
+
+ // No rounds in sandbox: neutralize the base round scheduler entirely.
+ scheduleRoundStart() {},
+ startRound() {},
+ endRound() {},
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe Sandbox! Press Q (or type !q) for the spawn menu.");
+ },
+
+ PlayerSpawn(ply) {
+ ensureBuildEnabled();
+ if (ply) ply.chat("Sandbox: !q spawn menu · !spawn crate");
+ },
+
+ Think() {}
+ };
+
+ gamemode.set(GM);
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
+// OpenVibe Sandbox — shared realm.
+// The spawn catalog lives here so both the server (which does the actual
+// spawning) and the client (which can render the Q menu) agree on the exact
+// same categories and prop ids without duplicating the list.
+(function () {
+ // Every prop id here must be allowlisted by the C++ ov_fortwars_spawn command,
+ // which is what actually creates the prop_physics entity server-side.
+ const SPAWN_CATALOG = [
+ {
+ category: "Build Props",
+ items: [
+ { id: "crate", label: "Wooden Crate" },
+ { id: "barrel", label: "Oil Drum" },
+ { id: "pallet", label: "Wood Pallet" },
+ { id: "fence", label: "Wood Fence" },
+ { id: "sheet", label: "Mattress" }
+ ]
+ }
+ ];
+
+ // Flatten to a quick id -> label lookup for validation / menu numbering.
+ const byId = Object.create(null);
+ const flat = [];
+ SPAWN_CATALOG.forEach(function (cat) {
+ cat.items.forEach(function (item) {
+ byId[item.id] = item;
+ flat.push(item);
+ });
+ });
+
+ globalThis.OVSandbox = {
+ SPAWN_CATALOG: SPAWN_CATALOG,
+ spawnItems: flat,
+ isSpawnable: function (id) { return !!byId[String(id || "").toLowerCase()]; },
+ labelFor: function (id) {
+ const item = byId[String(id || "").toLowerCase()];
+ return item ? item.label : id;
+ }
+ };
+})();
new file mode 100755
new file mode 100755
@@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
+#!/bin/bash
+# OpenVibe: Source - Game Client Launcher via GE-Proton10-34
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+STEAM_PATH="${OPENVIBE_STEAM_PATH:-/mnt/6tb/ssd_offload/home/workstation/.steam/debian-installation}"
+GE_PROTON="${OPENVIBE_GE_PROTON:-$STEAM_PATH/compatibilitytools.d/GE-Proton10-34}"
+STEAM_COMPAT_DATA="${OPENVIBE_STEAM_COMPAT_DATA:-$STEAM_PATH/steamapps/compatdata/243750}"
+HL2_EXE="${OPENVIBE_HL2_EXE:-/mnt/data-f/SteamLibrary/steamapps/common/Source SDK Base 2013 Multiplayer/hl2_win64.exe}"
+GAME_DIR="${OPENVIBE_GAME_DIR:-$ROOT/game/openvibe.games}"
+CLIENT_DLL="$GAME_DIR/bin/x64/client.dll"
+SERVER_DLL="$GAME_DIR/bin/x64/server.dll"
+
+if [ ! -f "$HL2_EXE" ]; then
+ echo "ERROR: hl2.exe not found at $HL2_EXE" >&2
+ exit 1
+fi
+
+if [ ! -d "$GE_PROTON" ]; then
+ echo "ERROR: GE-Proton not found at $GE_PROTON" >&2
+ exit 1
+fi
+
+CONNECT_ARGS=""
+if [ "${1:-}" != "" ] && [ "${2:-}" != "" ]; then
+ CONNECT_ARGS="+connect $1:$2"
+fi
+
+echo "Launching OpenVibe: Source via Proton..."
+echo " Game: $GAME_DIR"
+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
+ 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
+else
+ echo " WARNING: client.dll missing. Proton will not have OpenVibe client commands." >&2
+fi
+
+if [ ! -f "$SERVER_DLL" ]; then
+ echo " WARNING: server.dll missing for Windows listen/server use." >&2
+fi
+
+mkdir -p "$STEAM_COMPAT_DATA"
+
+export DISPLAY="${DISPLAY:-:0}"
+export STEAM_COMPAT_CLIENT_INSTALL_PATH="$STEAM_PATH"
+export STEAM_COMPAT_DATA_PATH="$STEAM_COMPAT_DATA"
+export SteamAppId="${SteamAppId:-243750}"
+export PROTON_LOG="${OPENVIBE_PROTON_LOG:-${PROTON_LOG:-1}}"
+
+# DXVK_ASYNC=1 races async shader pipeline compilation against DXVK swapchain
+# teardown on first launch; hl2.exe intermittently dies right after the first
+# frame with no error trace. Default off for reliability.
+export DXVK_ASYNC="${DXVK_ASYNC:-0}"
+
+exec "$GE_PROTON/proton" waitforexitandrun \
+ "$HL2_EXE" \
+ -game "$GAME_DIR" \
+ -console -dev -condebug -novid -sw -w 1280 -h 720 \
+ -port 27115 -clientport 27105 \
+ +developer 1 \
+ +con_logfile openvibe_proton_console.log \
+ +exec openvibe_proton_client.cfg \
+ $CONNECT_ARGS
new file mode 100755
new file mode 100755
@@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# OpenVibe Sandbox dev server (free-build + JS Q menu).
+#
+# Standalone launcher (does not use run-server.sh's fixed map_input) because the
+# sandbox needs to force the JS gamemode to "sandbox" AFTER the map is up:
+# loading the ov_hub map auto-execs cfg/ov_hub.cfg -> openvibe_hub.cfg, and the
+# JS runtime latches ov_mode on first tick. ov_js_reload re-reads ov_mode and
+# reloads the gamemode, so we set ov_mode + reload once the server is live.
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+SRCDS="${OPENVIBE_SRCDS:-$HOME/srcds/tf2}"
+MOD="$ROOT/game/openvibe.games"
+PORT="${OPENVIBE_SANDBOX_PORT:-27020}"
+MAP="${OPENVIBE_SANDBOX_MAP:-ov_hub}"
+MAXPLAYERS="${OPENVIBE_SANDBOX_MAXPLAYERS:-24}"
+BIND_IP="${OPENVIBE_BIND_IP:-127.0.0.1}"
+MAP_DELAY="${OPENVIBE_SRCDS_MAP_DELAY:-6}"
+RELOAD_DELAY="${OPENVIBE_SANDBOX_RELOAD_DELAY:-8}"
+
+if [[ ! -x "$SRCDS/srcds_linux64" ]]; then
+ echo "Missing 64-bit SRCDS at $SRCDS/srcds_linux64" >&2
+ exit 1
+fi
+if [[ ! -f "$MOD/maps/$MAP.bsp" ]]; then
+ echo "Missing map $MOD/maps/$MAP.bsp" >&2
+ exit 1
+fi
+
+cd "$SRCDS"
+printf '243750\n' > "$SRCDS/steam_appid.txt"
+
+COMPAT_LIB_DIR="${OPENVIBE_SRCDS_COMPAT_LIB_DIR:-/tmp/openvibe-srcds-compat-$USER}"
+if [[ ! -e /lib/i386-linux-gnu/libtinfo.so.5 && -e /lib/i386-linux-gnu/libtinfo.so.6 ]]; then
+ mkdir -p "$COMPAT_LIB_DIR"
+ ln -sfn /lib/i386-linux-gnu/libtinfo.so.6 "$COMPAT_LIB_DIR/libtinfo.so.5"
+fi
+if [[ ! -e /lib/i386-linux-gnu/libncurses.so.5 && -e /lib/i386-linux-gnu/libncurses.so.6 ]]; then
+ mkdir -p "$COMPAT_LIB_DIR"
+ ln -sfn /lib/i386-linux-gnu/libncurses.so.6 "$COMPAT_LIB_DIR/libncurses.so.5"
+fi
+
+export LD_LIBRARY_PATH="$COMPAT_LIB_DIR:$MOD/bin/linux64:.:$SRCDS/bin/linux64:$SRCDS/bin:${LD_LIBRARY_PATH:-}"
+
+server_cmd=(
+ ./srcds_linux64
+ -game "$MOD"
+ -console
+ -usercon
+ -ip "$BIND_IP"
+ -port "$PORT"
+ +clientport "$((PORT + 1000))"
+ +maxplayers "$MAXPLAYERS"
+ +tv_port "$((PORT + 2000))"
+ +exec openvibe_sandbox.cfg
+)
+
+printf -v server_command '%q ' "${server_cmd[@]}"
+
+console_input() {
+ sleep "$MAP_DELAY"
+ printf 'map %s\n' "$MAP"
+ # After the map is live and the JS runtime has started, force sandbox mode.
+ sleep "$RELOAD_DELAY"
+ printf 'ov_mode sandbox\n'
+ printf 'ov_js_reload\n'
+ while sleep 3600; do :; done
+}
+
+filter_output() {
+ sed -u \
+ -e '/WARNING: Failed to load 32-bit libtinfo\.so\.5 or libncurses\.so\.5\./d' \
+ -e '/Please install (lib32tinfo5 \/ ncurses-libs\.i686 \/ equivalent) to enable readline\./d'
+}
+
+if command -v script >/dev/null 2>&1; then
+ console_input | script -qfec "$server_command" /dev/null | filter_output
+else
+ console_input | "${server_cmd[@]}" | filter_output
+fi