openvibers/source.openvibe.games Commits
Add JS command registry and timers
Changes
@@@ -1,20 +1,5 @@--OV.log("bridge.js loaded");----globalThis.console = {-- log: (...args) => OV.log(args.map(String).join(" ")),-- warn: (...args) => OV.warn(args.map(String).join(" ")),-- error: (...args) => OV.error(args.map(String).join(" "))--};----globalThis.game = {-- mode: () => OV.getMode(),-- map: () => OV.getMapName(),-- time: () => OV.time(),-- broadcast: (msg) => OV.broadcast(String(msg)),-- serverCommand: (cmd) => OV.serverCommand(String(cmd))--};----globalThis.round = {-- start: () => OV.fireHook("RoundStart"),-- end: () => OV.fireHook("RoundEnd")--};++(function () {++ if (globalThis.OV && typeof OV.log === "function") {++ OV.log("bridge.js loaded");++ }++})();@@@ -1,39 +1,25 @@--const GM = {-- mode: "base",-- name: "OpenVibe Base",---- Initialize() {-- OV.log("Base Initialize fired");-- },---- MapInitialize(mapName) {-- OV.log(`Base MapInitialize: ${mapName}`);-- },---- PlayerInitialSpawn(ply) {-- ply.chat("Welcome to OpenVibe: Source.");-- },---- PlayerSpawn(_ply) {},---- PlayerDeath(_victim, _attacker) {},---- PlayerDisconnected(_ply) {},---- PlayerSay(_ply, _text) {-- return undefined;-- },---- ConsoleCommand(text) {-- OV.log(`ConsoleCommand: ${text}`);-- return undefined;-- },---- Think() {},----ConsoleCommand(text) {-- OV.log(`Base ConsoleCommand: ${text}`);-- }--};----gamemode.set(GM);++(function () {++ const BaseServerGM = {++ mode: "base",++ name: "OpenVibe Base",++++ Initialize() {++ OV.log("Base Initialize fired");++ },++++ MapInitialize(mapName) {++ OV.log(`Base MapInitialize: ${mapName}`);++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source.");++ },++++ PlayerSpawn(_ply) {},++ PlayerDeath(_victim, _attacker) {},++ PlayerDisconnected(_ply) {},++ Think() {}++ };++++ gamemode.set(BaseServerGM);++})();@@@ -1,24 +1,18 @@--const GM = {-- mode: "deathrun",-- name: "OpenVibe Deathrun",---- Initialize() {-- OV.log("Deathrun Initialize fired");-- },---- PlayerInitialSpawn(ply) {-- ply.chat("Deathrun: survive the traps and reach the finish.");-- },---- PlayerSay(ply, text) {-- if (text === "!finish") {-- ply.chat("Deathrun finish test.");-- OV.reward(ply, 50, 100, "deathrun_finish");++(function () {++ function registerCommands() {++ if (!globalThis.command) return;++ command.add("dr_status", "Show Deathrun status", function ({ ply, reply }) {++ reply(ply, `Deathrun JS online. map=${OV.getMapName()} players=${OV.players().length}`);return false;-- }---- return undefined;++ });}--};--gamemode.set(GM);++ const DeathrunGM = {++ mode: "deathrun",++ name: "OpenVibe Deathrun",++ Initialize() { OV.log("Deathrun Initialize fired"); registerCommands(); },++ PlayerInitialSpawn(ply) { ply.chat("Deathrun JS loaded. Try !dr_status"); },++ Think() {}++ };++ gamemode.set(DeathrunGM);++})();@@@ -1,29 +1,25 @@--const allowed = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);----const GM = {-- mode: "fortwars",-- name: "OpenVibe Fort Wars",---- Initialize() {-- OV.log("Fort Wars Initialize fired");-- },---- PlayerInitialSpawn(ply) {-- ply.chat("Fort Wars: build first, fight second.");-- },---- PlayerSay(ply, text) {-- if (!text.startsWith("!build ")) return undefined;---- const part = text.slice(7).trim();-- if (!allowed.has(part)) {-- ply.chat("Allowed: crate, barrel, pallet, fence, sheet");++(function () {++ function registerCommands() {++ if (!globalThis.command) return;++ command.add("fw_status", "Show Fort Wars status", function ({ ply, reply }) {++ reply(ply, `Fort Wars JS online. map=${OV.getMapName()} players=${OV.players().length}`);return false;-- }---- ply.runCommand(`ov_fortwars_spawn ${part}`);-- return false;++ });++ command.add("build", "Spawn an allowlisted Fort Wars prop", function ({ args, ply, reply }) {++ const choice = args[0] || "crate";++ if (!ply) return false;++ ply.runCommand(`ov_fortwars_spawn ${choice}`);++ reply(ply, `Trying Fort Wars prop: ${choice}`);++ return false;++ });}--};--gamemode.set(GM);++ const FortWarsGM = {++ mode: "fortwars",++ name: "OpenVibe Fort Wars",++ Initialize() { OV.log("Fort Wars Initialize fired"); registerCommands(); },++ PlayerInitialSpawn(ply) { ply.chat("Fort Wars JS loaded. Try !fw_status or !build crate"); },++ Think() {}++ };++ gamemode.set(FortWarsGM);++})();@@@ -1,55 +1,67 @@--const GM = {-- mode: "hub",-- name: "OpenVibe Hub",---- Initialize() {-- OV.log("Hub Initialize fired");-- },---- MapInitialize(mapName) {-- OV.log(`Map initialized: ${mapName}`);-- },---- PlayerInitialSpawn(ply) {-- ply.chat("Welcome to OpenVibe: Source JS runtime.");-- OV.broadcast(`${ply.name()} joined the hub.`);-- },---- PlayerSpawn(ply) {-- ply.chat("PlayerSpawn hook fired.");-- },---- PlayerSay(ply, text) {-- if (text === "!js") {-- ply.chat("JavaScript hooks are working.");-- return false;-- }++(function () {++ function registerHubCommands() {++ if (!globalThis.command) return;-- if (text === "!hp") {-- ply.chat(`Health: ${ply.health()}`);++ command.add("js", "Confirm JavaScript hooks are working", function ({ ply, reply }) {++ reply(ply, "JavaScript hooks are working.");return false;-- }++ });-- if (text === "!players") {-- ply.chat(`Players online: ${OV.players().length}`);++ command.add("hp", "Show current health", function ({ ply, reply }) {++ if (!ply) return false;++ reply(ply, `Health: ${ply.health()}`);return false;-- }++ });-- return undefined;-- },++ command.add("players", "Show player count", function ({ ply, reply }) {++ reply(ply, `Players online: ${OV.players().length}`);++ return false;++ });-- ConsoleCommand(text) {-- OV.log(`Hub ConsoleCommand: ${text}`);++ command.add("where", "Show current mode and map", function ({ ply, reply }) {++ reply(ply, `Mode=${OV.getMode()} map=${OV.getMapName()}`);++ return false;++ });-- if (text === "smoke") {-- OV.broadcast("OpenVibe embedded JS smoke test passed.");++ command.add("hub_status", "Broadcast hub status", function ({ ply, reply }) {++ reply(ply, `Hub OK. mode=${OV.getMode()} map=${OV.getMapName()} players=${OV.players().length}`);return false;-- }++ });++ }++++ const HubServerGM = {++ mode: "hub",++ name: "OpenVibe Hub",++++ Initialize() {++ OV.log("Hub Initialize fired");++ registerHubCommands();++ },++++ MapInitialize(mapName) {++ OV.log(`Hub MapInitialize: ${mapName}`);++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source JS runtime.");++ OV.broadcast(`${ply.name()} joined the hub.`);++ },++++ PlayerSpawn(ply) {++ ply.chat("PlayerSpawn hook fired.");++ },++++ PlayerSay(_ply, _text) {++ return undefined;++ },-- return undefined;-- },++ ConsoleCommand(text) {++ OV.log(`Hub ConsoleCommand fallback: ${text}`);++ return undefined;++ },-- Think() {}--};++ Think() {}++ };--gamemode.set(GM);++ gamemode.set(HubServerGM);++})();@@@ -1,36 +1,25 @@--const props = ["crate", "barrel", "chair", "bucket"];----function randomProp() {-- return props[Math.floor(Math.random() * props.length)];--}----const GM = {-- mode: "prophunt",-- name: "OpenVibe Prop Hunt",---- Initialize() {-- OV.log("Prop Hunt Initialize fired");-- },---- PlayerInitialSpawn(ply) {-- ply.chat("Prop Hunt: hide as props or hunt them down.");-- },---- PlayerSay(ply, text) {-- if (text === "!prop") {-- ply.runCommand(`ov_prophunt_disguise ${randomProp()}`);++(function () {++ function registerCommands() {++ if (!globalThis.command) return;++ command.add("ph_status", "Show Prop Hunt status", function ({ ply, reply }) {++ reply(ply, `Prop Hunt JS online. map=${OV.getMapName()} players=${OV.players().length}`);return false;-- }---- if (text.startsWith("!prop ")) {-- ply.runCommand(`ov_prophunt_disguise ${text.slice(6).trim()}`);++ });++ command.add("disguise", "Disguise as an allowlisted prop", function ({ args, ply, reply }) {++ const choice = args[0] || "crate";++ if (!ply) return false;++ ply.runCommand(`ov_prophunt_disguise ${choice}`);++ reply(ply, `Trying prop disguise: ${choice}`);return false;-- }---- return undefined;-- },---- Think() {}--};----gamemode.set(GM);++ });++ }++++ const PropHuntGM = {++ mode: "prophunt",++ name: "OpenVibe Prop Hunt",++ Initialize() { OV.log("Prop Hunt Initialize fired"); registerCommands(); },++ PlayerInitialSpawn(ply) { ply.chat("Prop Hunt JS loaded. Try !ph_status or !disguise crate"); },++ Think() {}++ };++ gamemode.set(PropHuntGM);++})();@@@ -1,30 +1,18 @@--const roles = new Map();----const GM = {-- mode: "traitortown",-- name: "OpenVibe Traitor Town",---- Initialize() {-- OV.log("Traitor Town Initialize fired");-- },---- PlayerInitialSpawn(ply) {-- ply.chat("Traitor Town: find the traitors before they find you.");-- roles.set(ply.userId(), "innocent");-- },---- PlayerSay(ply, text) {-- if (text === "!role") {-- ply.chat(`Your role: ${roles.get(ply.userId()) || "none"}`);++(function () {++ function registerCommands() {++ if (!globalThis.command) return;++ command.add("ttt_status", "Show Traitor Town status", function ({ ply, reply }) {++ reply(ply, `Traitor Town JS online. map=${OV.getMapName()} players=${OV.players().length}`);return false;-- }---- return undefined;-- },---- PlayerDeath(victim, attacker) {-- OV.broadcast(`${victim.name()} died.`);++ });}--};--gamemode.set(GM);++ const TraitorTownGM = {++ mode: "traitortown",++ name: "OpenVibe Traitor Town",++ Initialize() { OV.log("Traitor Town Initialize fired"); registerCommands(); },++ PlayerInitialSpawn(ply) { ply.chat("Traitor Town JS loaded. Try !ttt_status"); },++ Think() {}++ };++ gamemode.set(TraitorTownGM);++})();@@@ -128,6 +128,12 @@ bool COpenVibeJSRuntime::LoadCoreFiles()if (!LoadFile("js/bridge.js"))return false;++ if (!LoadFile("js/core/command.js"))++ return false;++++ if (!LoadFile("js/core/timer.js"))++ return false;++return true;}nnew file mode 100755@@@ -0,0 +1,713 @@++#!/usr/bin/env bash++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++SDK="${OPENVIBE_SDK:-$ROOT/engine/source-sdk-2013}"++STAMP="$(date +%Y%m%d-%H%M%S)"++++cd "$ROOT"++++echo "[openvibe] fix JS global-scope redeclare and continue command/timer phase"++echo "[openvibe] root=$ROOT"++echo "[openvibe] sdk=$SDK"++++backup_file() {++ local file="$1"++ [[ -f "$file" ]] && cp "$file" "$file.bak.$STAMP"++}++++mkdir -p \++ game/openvibe.games/js/core \++ game/openvibe.games/js/gamemodes/base \++ game/openvibe.games/js/gamemodes/hub \++ game/openvibe.games/js/gamemodes/prophunt \++ game/openvibe.games/js/gamemodes/deathrun \++ game/openvibe.games/js/gamemodes/fortwars \++ game/openvibe.games/js/gamemodes/traitortown \++ tools++++echo "[openvibe] writing canonical command.js"++backup_file game/openvibe.games/js/core/command.js++cat > game/openvibe.games/js/core/command.js <<'JS'++(function () {++ const commands = new Map();++++ function say(target, message) {++ const text = String(message);++ if (target && typeof target.chat === "function") {++ target.chat(text);++ } else {++ OV.log(text);++ }++ }++++ function normalizeName(name) {++ return String(name || "").trim().replace(/^[!/]+/, "").toLowerCase();++ }++++ function parse(text) {++ const raw = String(text || "").trim();++ const withoutPrefix = raw.replace(/^[!/]+/, "");++ const parts = withoutPrefix.length ? withoutPrefix.split(/\s+/) : [];++ const name = normalizeName(parts.shift() || "");++ return { raw, name, args: parts };++ }++++ globalThis.command = {++ add(name, descriptionOrFn, maybeFn) {++ const key = normalizeName(name);++ if (!key) throw new Error("command.add requires command name");++++ let description = "";++ let fn = maybeFn;++++ if (typeof descriptionOrFn === "function") {++ fn = descriptionOrFn;++ } else {++ description = String(descriptionOrFn || "");++ }++++ if (typeof fn !== "function") throw new Error(`command '${key}' requires function`);++ commands.set(key, { name: key, description, fn });++ },++++ remove(name) {++ return commands.delete(normalizeName(name));++ },++++ has(name) {++ return commands.has(normalizeName(name));++ },++++ list() {++ return Array.from(commands.values()).map((entry) => ({++ name: entry.name,++ description: entry.description++ }));++ },++++ run(source, text, ...extra) {++ const parsed = parse(text);++ if (!parsed.name) return undefined;++++ const entry = commands.get(parsed.name);++ if (!entry) {++ say(source, `Unknown command: ${parsed.name}. Try !help.`);++ return source ? false : undefined;++ }++++ try {++ const result = entry.fn(source, parsed.args, parsed.raw, ...extra);++ return result === undefined ? false : result;++ } catch (err) {++ OV.error(`[command:${parsed.name}] ${err && err.stack ? err.stack : err}`);++ say(source, `Command failed: ${parsed.name}`);++ return false;++ }++ }++ };++++ hook.add("ConsoleCommand", "OpenVibeCommandConsole", function (text) {++ return command.run(null, text);++ });++++ hook.add("PlayerSay", "OpenVibeCommandChat", function (ply, text) {++ const msg = String(text || "").trim();++ if (!msg.startsWith("!")) return undefined;++ return command.run(ply, msg.slice(1));++ });++})();++JS++++echo "[openvibe] writing canonical timer.js"++backup_file game/openvibe.games/js/core/timer.js++cat > game/openvibe.games/js/core/timer.js <<'JS'++(function () {++ const timers = new Map();++ let nextId = 1;++++ function now() {++ if (typeof OV !== "undefined" && typeof OV.time === "function") return Number(OV.time()) || 0;++ return Date.now() / 1000;++ }++++ function makeId() {++ return `timer_${nextId++}`;++ }++++ globalThis.timer = {++ create(id, delay, repetitions, fn) {++ if (typeof id !== "string" || !id) throw new Error("timer.create requires id");++ if (typeof fn !== "function") throw new Error("timer.create requires callback");++++ const safeDelay = Math.max(0, Number(delay) || 0);++ const reps = repetitions === undefined ? 1 : Number(repetitions);++++ timers.set(id, {++ id,++ delay: safeDelay,++ repetitions: reps,++ fn,++ next: now() + safeDelay,++ running: true++ });++++ return id;++ },++++ simple(delay, fn) {++ return timer.create(makeId(), delay, 1, fn);++ },++++ remove(id) {++ return timers.delete(String(id));++ },++++ exists(id) {++ return timers.has(String(id));++ },++++ count() {++ return timers.size;++ },++++ tick() {++ const t = now();++++ for (const [id, item] of Array.from(timers.entries())) {++ if (!item.running || t < item.next) continue;++++ try {++ item.fn();++ } catch (err) {++ OV.error(`[timer:${id}] ${err && err.stack ? err.stack : err}`);++ }++++ if (item.repetitions > 0) item.repetitions -= 1;++++ if (item.repetitions === 0) {++ timers.delete(id);++ } else {++ item.next = t + item.delay;++ }++ }++ },++++ clear() {++ timers.clear();++ }++ };++++ hook.add("Think", "OpenVibeTimerThink", function () {++ timer.tick();++ return undefined;++ });++})();++JS++++echo "[openvibe] writing scoped base gamemode"++backup_file game/openvibe.games/js/gamemodes/base/server.js++cat > game/openvibe.games/js/gamemodes/base/server.js <<'JS'++(function () {++ function say(target, message) {++ if (target && typeof target.chat === "function") target.chat(String(message));++ else OV.log(String(message));++ }++++ command.add("help", "List available OpenVibe commands.", function (source) {++ const names = command.list().map((entry) => `!${entry.name}`).sort().join(", ");++ say(source, `Commands: ${names}`);++ return false;++ });++++ command.add("smoke", "Run embedded JS smoke test.", function (source) {++ const message = "OpenVibe embedded JS smoke test passed.";++ if (source) say(source, message);++ OV.broadcast(message);++ return false;++ });++++ command.add("timer_smoke", "Run timer smoke test.", function (source) {++ timer.simple(0, function () {++ OV.broadcast("OpenVibe timer smoke fired.");++ });++ timer.tick();++ say(source, "Timer smoke scheduled.");++ return false;++ });++++ const GM = {++ mode: "base",++ name: "OpenVibe Base",++++ Initialize() {++ OV.log("Base Initialize fired");++ },++++ MapInitialize(mapName) {++ OV.log(`Base MapInitialize: ${mapName}`);++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source.");++ },++++ PlayerSpawn(_ply) {},++++ PlayerDeath(_victim, _attacker) {},++++ PlayerDisconnected(_ply) {},++++ PlayerSay(ply, text) {++ const msg = String(text || "").trim();++ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));++ return undefined;++ },++++ ConsoleCommand(text) {++ return command.run(null, text);++ },++++ Think() {++ timer.tick();++ }++ };++++ gamemode.set(GM);++})();++JS++++echo "[openvibe] writing scoped hub gamemode"++backup_file game/openvibe.games/js/gamemodes/hub/server.js++cat > game/openvibe.games/js/gamemodes/hub/server.js <<'JS'++(function () {++ command.add("hub_status", "Print hub runtime status.", function (source) {++ const message = `Hub mode online. Map=${OV.getMapName()} Players=${OV.players().length}`;++ if (source && typeof source.chat === "function") source.chat(message);++ else OV.log(message);++ return false;++ });++++ command.add("where", "Show current map/mode.", function (source) {++ const message = `You are on ${OV.getMapName()} in ${OV.getMode()} mode.`;++ if (source && typeof source.chat === "function") source.chat(message);++ else OV.log(message);++ return false;++ });++++ const GM = {++ mode: "hub",++ name: "OpenVibe Hub",++++ Initialize() {++ OV.log("Hub Initialize fired");++ },++++ MapInitialize(mapName) {++ OV.log(`Map initialized: ${mapName}`);++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source JS runtime.");++ OV.broadcast(`${ply.name()} joined the hub.`);++ },++++ PlayerSpawn(_ply) {},++++ PlayerSay(ply, text) {++ const msg = String(text || "").trim();++++ if (msg === "!js") {++ ply.chat("JavaScript hooks are working.");++ return false;++ }++++ if (msg === "!hp") {++ ply.chat(`Health: ${ply.health()}`);++ return false;++ }++++ if (msg === "!players") {++ ply.chat(`Players online: ${OV.players().length}`);++ return false;++ }++++ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));++ return undefined;++ },++++ ConsoleCommand(text) {++ OV.log(`Hub ConsoleCommand: ${text}`);++ return command.run(null, text);++ },++++ Think() {++ timer.tick();++ }++ };++++ gamemode.set(GM);++})();++JS++++echo "[openvibe] writing scoped prototype gamemodes"++cat > game/openvibe.games/js/gamemodes/prophunt/server.js <<'JS'++(function () {++ const props = ["crate", "barrel", "chair", "bucket"];++++ function randomProp() {++ return props[Math.floor(Math.random() * props.length)];++ }++++ command.add("prop", "Prop Hunt disguise command.", function (ply, args) {++ if (!ply) {++ OV.log("prop command requires player");++ return false;++ }++++ const prop = args[0] || randomProp();++ ply.runCommand(`ov_prophunt_disguise ${prop}`);++ return false;++ });++++ const GM = {++ mode: "prophunt",++ name: "OpenVibe Prop Hunt",++++ Initialize() {++ OV.log("Prop Hunt Initialize fired");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Prop Hunt: hide as props or hunt them down.");++ },++++ PlayerSay(ply, text) {++ const msg = String(text || "").trim();++ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));++ return undefined;++ },++++ Think() {++ timer.tick();++ }++ };++++ gamemode.set(GM);++})();++JS++++cat > game/openvibe.games/js/gamemodes/deathrun/server.js <<'JS'++(function () {++ command.add("finish", "Deathrun finish smoke command.", function (ply) {++ if (ply) ply.chat("Deathrun finish test.");++ return false;++ });++++ const GM = {++ mode: "deathrun",++ name: "OpenVibe Deathrun",++++ Initialize() {++ OV.log("Deathrun Initialize fired");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Deathrun: survive the traps and reach the finish.");++ },++++ PlayerSay(ply, text) {++ const msg = String(text || "").trim();++ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));++ return undefined;++ },++++ Think() {++ timer.tick();++ }++ };++++ gamemode.set(GM);++})();++JS++++cat > game/openvibe.games/js/gamemodes/fortwars/server.js <<'JS'++(function () {++ const allowed = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);++++ command.add("build", "Fort Wars build command.", function (ply, args) {++ if (!ply) {++ OV.log("build command requires player");++ return false;++ }++++ const part = args[0] || "crate";++ if (!allowed.has(part)) {++ ply.chat("Allowed: crate, barrel, pallet, fence, sheet");++ return false;++ }++++ ply.runCommand(`ov_fortwars_spawn ${part}`);++ return false;++ });++++ const GM = {++ mode: "fortwars",++ name: "OpenVibe Fort Wars",++++ Initialize() {++ OV.log("Fort Wars Initialize fired");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Fort Wars: build first, fight second.");++ },++++ PlayerSay(ply, text) {++ const msg = String(text || "").trim();++ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));++ return undefined;++ },++++ Think() {++ timer.tick();++ }++ };++++ gamemode.set(GM);++})();++JS++++cat > game/openvibe.games/js/gamemodes/traitortown/server.js <<'JS'++(function () {++ const roles = new Map();++++ command.add("role", "Show your Traitor Town role.", function (ply) {++ if (!ply) {++ OV.log("role command requires player");++ return false;++ }++++ ply.chat(`Your role: ${roles.get(ply.userId()) || "innocent"}`);++ return false;++ });++++ const GM = {++ mode: "traitortown",++ name: "OpenVibe Traitor Town",++++ Initialize() {++ OV.log("Traitor Town Initialize fired");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Traitor Town: find the traitors before they find you.");++ roles.set(ply.userId(), "innocent");++ },++++ PlayerSay(ply, text) {++ const msg = String(text || "").trim();++ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));++ return undefined;++ },++++ PlayerDeath(victim, _attacker) {++ OV.broadcast(`${victim.name()} died.`);++ },++++ Think() {++ timer.tick();++ }++ };++++ gamemode.set(GM);++})();++JS++++echo "[openvibe] patching runtime core load order"++backup_file sdk/openvibe/shared/ov_js_runtime.cpp++python3 <<'PY'++from pathlib import Path++import re++++p = Path("sdk/openvibe/shared/ov_js_runtime.cpp")++s = p.read_text()++++new_func = r'''bool COpenVibeJSRuntime::LoadCoreFiles()++{++ if (!LoadFile("js/core/hook.js"))++ return false;++++ if (!LoadFile("js/core/gamemode.js"))++ return false;++++ if (!LoadFile("js/bridge.js"))++ return false;++++ if (!LoadFile("js/core/command.js"))++ return false;++++ if (!LoadFile("js/core/timer.js"))++ return false;++++ return true;++}'''++++s2 = re.sub(++ r'bool\s+COpenVibeJSRuntime::LoadCoreFiles\s*\(\s*\)\s*\{.*?\n\}',++ new_func,++ s,++ count=1,++ flags=re.S,++)++++if s2 == s:++ raise SystemExit("Could not patch COpenVibeJSRuntime::LoadCoreFiles")++++p.write_text(s2)++PY++++echo "[openvibe] writing robust Node JS core smoke test"++backup_file tools/smoke-js-core-node.mjs++cat > tools/smoke-js-core-node.mjs <<'MJS'++import fs from "node:fs";++import path from "node:path";++import vm from "node:vm";++++const root = process.env.OPENVIBE_ROOT || process.cwd();++let fakeTime = 0;++const logs = [];++const broadcasts = [];++const chats = [];++const serverCommands = [];++++const context = {++ console,++ globalThis: null,++ OV: {++ log: (msg) => logs.push(String(msg)),++ warn: (msg) => logs.push(`WARN ${String(msg)}`),++ error: (msg) => logs.push(`ERROR ${String(msg)}`),++ getMode: () => "hub",++ getMapName: () => "ov_hub",++ time: () => fakeTime,++ broadcast: (msg) => broadcasts.push(String(msg)),++ players: () => [mockPlayer],++ playerByUserId: (id) => Number(id) === 1 ? mockPlayer : null,++ serverCommand: (cmd) => serverCommands.push(String(cmd)),++ fireHook: (...args) => context.gamemode.call(...args),++ reward: () => undefined,++ endMatch: () => undefined++ }++};++++const mockPlayer = {++ userId: () => 1,++ entIndex: () => 1,++ steamId: () => "STEAM_0:1:1",++ name: () => "SmokePlayer",++ health: () => 100,++ setHealth: () => undefined,++ team: () => 0,++ setTeam: () => undefined,++ chat: (msg) => chats.push(String(msg)),++ runCommand: (cmd) => serverCommands.push(String(cmd))++};++++context.globalThis = context;++vm.createContext(context);++++function load(relativePath) {++ const full = path.join(root, relativePath);++ const source = fs.readFileSync(full, "utf8");++ vm.runInContext(source, context, { filename: relativePath });++}++++for (const file of [++ "game/openvibe.games/js/core/hook.js",++ "game/openvibe.games/js/core/gamemode.js",++ "game/openvibe.games/js/bridge.js",++ "game/openvibe.games/js/core/command.js",++ "game/openvibe.games/js/core/timer.js",++ "game/openvibe.games/js/gamemodes/base/server.js",++ "game/openvibe.games/js/gamemodes/hub/server.js"++]) {++ load(file);++}++++if (!context.hook) throw new Error("hook global missing");++if (!context.gamemode) throw new Error("gamemode global missing");++if (!context.command) throw new Error("command global missing");++if (!context.timer) throw new Error("timer global missing");++if (!context.gamemode.get()) throw new Error("gamemode was not set");++++context.gamemode.call("Initialize");++context.gamemode.call("MapInitialize", "ov_hub");++context.gamemode.call("ConsoleCommand", "help");++context.gamemode.call("ConsoleCommand", "smoke");++context.gamemode.call("ConsoleCommand", "timer_smoke");++context.gamemode.call("ConsoleCommand", "hub_status");++context.gamemode.call("PlayerInitialSpawn", mockPlayer);++context.gamemode.call("PlayerSay", mockPlayer, "!help");++context.gamemode.call("PlayerSay", mockPlayer, "!where");++context.gamemode.call("PlayerSay", mockPlayer, "!js");++context.gamemode.call("PlayerSay", mockPlayer, "!hp");++context.gamemode.call("PlayerSay", mockPlayer, "!players");++fakeTime += 1;++context.gamemode.call("Think");++context.timer.tick();++++const combined = [...logs, ...broadcasts, ...chats, ...serverCommands].join("\n");++++for (const expected of [++ "Hub Initialize fired",++ "OpenVibe embedded JS smoke test passed.",++ "OpenVibe timer smoke fired.",++ "JavaScript hooks are working.",++ "Health: 100",++ "Players online: 1",++ "You are on ov_hub in hub mode."++]) {++ if (!combined.includes(expected)) {++ console.error(combined);++ throw new Error(`missing expected smoke output: ${expected}`);++ }++}++++console.log("[openvibe-smoke] JS core command/timer smoke passed");++console.log(`[openvibe-smoke] logs=${logs.length} broadcasts=${broadcasts.length} chats=${chats.length}`);++MJS++++echo "[openvibe] run JS core smoke test"++node tools/smoke-js-core-node.mjs++++echo "[openvibe] apply SDK patch"++tools/apply-openvibe-sdk.sh++++echo "[openvibe] build SDK"++tools/build-sdk-linux.sh 2>&1 | tee "$HOME/ov-build.log"++++echo "[openvibe] setup OpenVibe bin"++tools/setup-openvibe-bin.sh++++echo++printf '%s\n' "[openvibe] JS command/timer phase complete."++printf '%s\n' ""++printf '%s\n' "Runtime test:"++printf '%s\n' " OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh"++printf '%s\n' ""++printf '%s\n' "Server console tests:"++printf '%s\n' " ov_js_status"++printf '%s\n' " ov_js_fire Initialize"++printf '%s\n' " ov_js_cmd help"++printf '%s\n' " ov_js_cmd smoke"++printf '%s\n' " ov_js_cmd timer_smoke"++printf '%s\n' " ov_js_cmd hub_status"++printf '%s\n' ""++printf '%s\n' "Client tests:"++printf '%s\n' " connect 127.0.0.1:27015"++printf '%s\n' " say !help"++printf '%s\n' " say !js"++printf '%s\n' " say !hp"++printf '%s\n' " say !players"++printf '%s\n' " say !where"nnew file mode 100755@@@ -0,0 +1,676 @@++#!/usr/bin/env bash++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++SDK="${OPENVIBE_SDK:-$ROOT/engine/source-sdk-2013}"++STAMP="$(date +%Y%m%d-%H%M%S)"++++cd "$ROOT"++++echo "[openvibe] fix LoadCoreFiles patch + finish JS command/timer phase"++echo "[openvibe] root=$ROOT"++echo "[openvibe] sdk=$SDK"++++backup_file() {++ local f="$1"++ [[ -f "$f" ]] && cp "$f" "$f.bak.$STAMP"++}++++mkdir -p game/openvibe.games/js/core++mkdir -p game/openvibe.games/js/gamemodes/base++mkdir -p game/openvibe.games/js/gamemodes/hub++mkdir -p game/openvibe.games/js/gamemodes/prophunt++mkdir -p game/openvibe.games/js/gamemodes/deathrun++mkdir -p game/openvibe.games/js/gamemodes/fortwars++mkdir -p game/openvibe.games/js/gamemodes/traitortown++mkdir -p tools++++echo "[openvibe] writing canonical JS core files"++++cat > game/openvibe.games/js/core/hook.js <<'JS'++(function () {++ const buckets = Object.create(null);++++ function ensure(name) {++ name = String(name || "");++ if (!buckets[name]) buckets[name] = [];++ return buckets[name];++ }++++ globalThis.hook = {++ add(name, id, fn) {++ if (!name || !id || typeof fn !== "function") {++ throw new Error("hook.add(name, id, fn) requires a hook name, id, and function");++ }++++ const list = ensure(name);++ const sid = String(id);++ for (let i = list.length - 1; i >= 0; --i) {++ if (list[i].id === sid) list.splice(i, 1);++ }++ list.push({ id: sid, fn });++ },++++ remove(name, id) {++ const list = buckets[String(name || "")];++ if (!list) return;++ const sid = String(id || "");++ for (let i = list.length - 1; i >= 0; --i) {++ if (list[i].id === sid) list.splice(i, 1);++ }++ },++++ run(name, ...args) {++ const list = buckets[String(name || "")];++ if (!list) return undefined;++++ const snapshot = list.slice();++ for (const item of snapshot) {++ const result = item.fn(...args);++ if (result !== undefined) return result;++ }++++ return undefined;++ },++++ list(name) {++ if (name) return (buckets[String(name)] || []).map((item) => item.id);++ const out = Object.create(null);++ for (const key of Object.keys(buckets)) out[key] = buckets[key].map((item) => item.id);++ return out;++ }++ };++})();++JS++++cat > game/openvibe.games/js/core/gamemode.js <<'JS'++(function () {++ let current = null;++++ globalThis.gamemode = {++ set(gm) {++ if (!gm || typeof gm !== "object") throw new Error("gamemode.set(gm) requires an object");++ current = gm;++ return gm;++ },++++ get() {++ return current;++ },++++ call(name, ...args) {++ const hookResult = hook.run(name, ...args);++ if (hookResult !== undefined) return hookResult;++++ if (current && typeof current[name] === "function") {++ return current[name](...args);++ }++++ return undefined;++ }++ };++})();++JS++++cat > game/openvibe.games/js/bridge.js <<'JS'++(function () {++ if (globalThis.OV && typeof OV.log === "function") {++ OV.log("bridge.js loaded");++ }++})();++JS++++cat > game/openvibe.games/js/core/command.js <<'JS'++(function () {++ const commands = Object.create(null);++++ function normalize(name) {++ return String(name || "").trim().replace(/^!+/, "").toLowerCase();++ }++++ function split(text) {++ const raw = String(text || "").trim();++ if (!raw) return { name: "", args: [], raw: "" };++ const parts = raw.split(/\s+/g);++ const name = normalize(parts.shift());++ return { name, args: parts, raw };++ }++++ function reply(ply, msg) {++ if (ply && typeof ply.chat === "function") ply.chat(String(msg));++ else if (globalThis.OV && typeof OV.broadcast === "function") OV.broadcast(String(msg));++ else if (globalThis.OV && typeof OV.log === "function") OV.log(String(msg));++ }++++ function listNames() {++ return Object.keys(commands).sort();++ }++++ globalThis.command = {++ add(name, helpOrFn, maybeFn) {++ const key = normalize(name);++ if (!key) throw new Error("command.add requires a command name");++++ let help = "";++ let fn = helpOrFn;++ if (typeof helpOrFn === "string") {++ help = helpOrFn;++ fn = maybeFn;++ }++++ if (typeof fn !== "function") throw new Error("command.add requires a function");++ commands[key] = { help, fn };++ },++++ remove(name) {++ delete commands[normalize(name)];++ },++++ has(name) {++ return !!commands[normalize(name)];++ },++++ list: listNames,++++ run(ply, text) {++ const parsed = split(text);++ if (!parsed.name) return undefined;++++ const entry = commands[parsed.name];++ if (!entry) {++ reply(ply, `Unknown command: ${parsed.name}. Try !help`);++ return false;++ }++++ const result = entry.fn({ ply, name: parsed.name, args: parsed.args, raw: parsed.raw, reply });++ return result === undefined ? false : result;++ },++++ dispatchConsole(text) {++ return this.run(null, text);++ }++ };++++ hook.add("PlayerSay", "OpenVibeCommandRegistry", function (ply, text) {++ const msg = String(text || "").trim();++ if (!msg.startsWith("!")) return undefined;++ return command.run(ply, msg.slice(1));++ });++++ hook.add("ConsoleCommand", "OpenVibeConsoleCommandRegistry", function (text) {++ return command.dispatchConsole(text);++ });++++ command.add("help", "List OpenVibe JS commands", function ({ reply, ply }) {++ reply(ply, `Commands: ${listNames().map((name) => `!${name}`).join(", ")}`);++ return false;++ });++++ command.add("smoke", "Run the embedded JS smoke test", function ({ reply, ply }) {++ reply(ply, "OpenVibe embedded JS smoke test passed.");++ return false;++ });++++ command.add("timer_smoke", "Run the JS timer smoke test", function ({ reply, ply }) {++ reply(ply, "Timer smoke scheduled.");++ if (globalThis.timer && typeof timer.simple === "function") {++ timer.simple(0.05, function () {++ if (globalThis.OV && typeof OV.broadcast === "function") {++ OV.broadcast("OpenVibe JS timer smoke fired.");++ }++ });++ }++ return false;++ });++})();++JS++++cat > game/openvibe.games/js/core/timer.js <<'JS'++(function () {++ const timers = Object.create(null);++ let nextAutoId = 1;++++ function now() {++ if (globalThis.OV && typeof OV.time === "function") return Number(OV.time()) || 0;++ return Date.now() / 1000;++ }++++ function create(id, delay, reps, fn) {++ if (!id) id = `timer_${nextAutoId++}`;++ if (typeof fn !== "function") throw new Error("timer.create requires a callback");++++ delay = Math.max(0, Number(delay) || 0);++ reps = Number(reps);++ if (!Number.isFinite(reps)) reps = 1;++++ timers[String(id)] = {++ id: String(id),++ delay,++ reps,++ count: 0,++ next: now() + delay,++ fn++ };++++ return String(id);++ }++++ function remove(id) {++ delete timers[String(id || "")];++ }++++ function tick() {++ const t = now();++ for (const id of Object.keys(timers)) {++ const item = timers[id];++ if (!item || t < item.next) continue;++++ item.count += 1;++ try {++ item.fn(item.count, id);++ } catch (err) {++ if (globalThis.OV && typeof OV.error === "function") OV.error(`timer ${id}: ${err && err.message ? err.message : err}`);++ delete timers[id];++ continue;++ }++++ if (item.reps > 0 && item.count >= item.reps) {++ delete timers[id];++ } else {++ item.next = t + item.delay;++ }++ }++ }++++ globalThis.timer = {++ create,++ remove,++ exists(id) {++ return !!timers[String(id || "")];++ },++ simple(delay, fn) {++ return create(`simple_${nextAutoId++}`, delay, 1, fn);++ },++ tick,++ list() {++ return Object.keys(timers).sort();++ }++ };++++ hook.add("Think", "OpenVibeTimerThink", tick);++})();++JS++++echo "[openvibe] writing scoped gamemode scripts"++++cat > game/openvibe.games/js/gamemodes/base/server.js <<'JS'++(function () {++ const BaseServerGM = {++ mode: "base",++ name: "OpenVibe Base",++++ Initialize() {++ OV.log("Base Initialize fired");++ },++++ MapInitialize(mapName) {++ OV.log(`Base MapInitialize: ${mapName}`);++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source.");++ },++++ PlayerSpawn(_ply) {},++ PlayerDeath(_victim, _attacker) {},++ PlayerDisconnected(_ply) {},++ Think() {}++ };++++ gamemode.set(BaseServerGM);++})();++JS++++cat > game/openvibe.games/js/gamemodes/hub/server.js <<'JS'++(function () {++ function registerHubCommands() {++ if (!globalThis.command) return;++++ command.add("js", "Confirm JavaScript hooks are working", function ({ ply, reply }) {++ reply(ply, "JavaScript hooks are working.");++ return false;++ });++++ command.add("hp", "Show current health", function ({ ply, reply }) {++ if (!ply) return false;++ reply(ply, `Health: ${ply.health()}`);++ return false;++ });++++ command.add("players", "Show player count", function ({ ply, reply }) {++ reply(ply, `Players online: ${OV.players().length}`);++ return false;++ });++++ command.add("where", "Show current mode and map", function ({ ply, reply }) {++ reply(ply, `Mode=${OV.getMode()} map=${OV.getMapName()}`);++ return false;++ });++++ command.add("hub_status", "Broadcast hub status", function ({ ply, reply }) {++ reply(ply, `Hub OK. mode=${OV.getMode()} map=${OV.getMapName()} players=${OV.players().length}`);++ return false;++ });++ }++++ const HubServerGM = {++ mode: "hub",++ name: "OpenVibe Hub",++++ Initialize() {++ OV.log("Hub Initialize fired");++ registerHubCommands();++ },++++ MapInitialize(mapName) {++ OV.log(`Hub MapInitialize: ${mapName}`);++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source JS runtime.");++ OV.broadcast(`${ply.name()} joined the hub.`);++ },++++ PlayerSpawn(ply) {++ ply.chat("PlayerSpawn hook fired.");++ },++++ PlayerSay(_ply, _text) {++ return undefined;++ },++++ ConsoleCommand(text) {++ OV.log(`Hub ConsoleCommand fallback: ${text}`);++ return undefined;++ },++++ Think() {}++ };++++ gamemode.set(HubServerGM);++})();++JS++++cat > game/openvibe.games/js/gamemodes/prophunt/server.js <<'JS'++(function () {++ function registerCommands() {++ if (!globalThis.command) return;++ command.add("ph_status", "Show Prop Hunt status", function ({ ply, reply }) {++ reply(ply, `Prop Hunt JS online. map=${OV.getMapName()} players=${OV.players().length}`);++ return false;++ });++ command.add("disguise", "Disguise as an allowlisted prop", function ({ args, ply, reply }) {++ const choice = args[0] || "crate";++ if (!ply) return false;++ ply.runCommand(`ov_prophunt_disguise ${choice}`);++ reply(ply, `Trying prop disguise: ${choice}`);++ return false;++ });++ }++++ const PropHuntGM = {++ mode: "prophunt",++ name: "OpenVibe Prop Hunt",++ Initialize() { OV.log("Prop Hunt Initialize fired"); registerCommands(); },++ PlayerInitialSpawn(ply) { ply.chat("Prop Hunt JS loaded. Try !ph_status or !disguise crate"); },++ Think() {}++ };++ gamemode.set(PropHuntGM);++})();++JS++++cat > game/openvibe.games/js/gamemodes/deathrun/server.js <<'JS'++(function () {++ function registerCommands() {++ if (!globalThis.command) return;++ command.add("dr_status", "Show Deathrun status", function ({ ply, reply }) {++ reply(ply, `Deathrun JS online. map=${OV.getMapName()} players=${OV.players().length}`);++ return false;++ });++ }++++ const DeathrunGM = {++ mode: "deathrun",++ name: "OpenVibe Deathrun",++ Initialize() { OV.log("Deathrun Initialize fired"); registerCommands(); },++ PlayerInitialSpawn(ply) { ply.chat("Deathrun JS loaded. Try !dr_status"); },++ Think() {}++ };++ gamemode.set(DeathrunGM);++})();++JS++++cat > game/openvibe.games/js/gamemodes/fortwars/server.js <<'JS'++(function () {++ function registerCommands() {++ if (!globalThis.command) return;++ command.add("fw_status", "Show Fort Wars status", function ({ ply, reply }) {++ reply(ply, `Fort Wars JS online. map=${OV.getMapName()} players=${OV.players().length}`);++ return false;++ });++ command.add("build", "Spawn an allowlisted Fort Wars prop", function ({ args, ply, reply }) {++ const choice = args[0] || "crate";++ if (!ply) return false;++ ply.runCommand(`ov_fortwars_spawn ${choice}`);++ reply(ply, `Trying Fort Wars prop: ${choice}`);++ return false;++ });++ }++++ const FortWarsGM = {++ mode: "fortwars",++ name: "OpenVibe Fort Wars",++ Initialize() { OV.log("Fort Wars Initialize fired"); registerCommands(); },++ PlayerInitialSpawn(ply) { ply.chat("Fort Wars JS loaded. Try !fw_status or !build crate"); },++ Think() {}++ };++ gamemode.set(FortWarsGM);++})();++JS++++cat > game/openvibe.games/js/gamemodes/traitortown/server.js <<'JS'++(function () {++ function registerCommands() {++ if (!globalThis.command) return;++ command.add("ttt_status", "Show Traitor Town status", function ({ ply, reply }) {++ reply(ply, `Traitor Town JS online. map=${OV.getMapName()} players=${OV.players().length}`);++ return false;++ });++ }++++ const TraitorTownGM = {++ mode: "traitortown",++ name: "OpenVibe Traitor Town",++ Initialize() { OV.log("Traitor Town Initialize fired"); registerCommands(); },++ PlayerInitialSpawn(ply) { ply.chat("Traitor Town JS loaded. Try !ttt_status"); },++ Think() {}++ };++ gamemode.set(TraitorTownGM);++})();++JS++++echo "[openvibe] robustly replacing COpenVibeJSRuntime::LoadCoreFiles"++++backup_file sdk/openvibe/shared/ov_js_runtime.cpp++++python3 <<'PY'++from pathlib import Path++++p = Path("sdk/openvibe/shared/ov_js_runtime.cpp")++s = p.read_text()++++sig = "bool COpenVibeJSRuntime::LoadCoreFiles()"++idx = s.find(sig)++if idx < 0:++ raise SystemExit("Could not find COpenVibeJSRuntime::LoadCoreFiles signature")++++brace = s.find("{", idx)++if brace < 0:++ raise SystemExit("Could not find COpenVibeJSRuntime::LoadCoreFiles opening brace")++++depth = 0++end = None++for i in range(brace, len(s)):++ ch = s[i]++ if ch == "{":++ depth += 1++ elif ch == "}":++ depth -= 1++ if depth == 0:++ end = i + 1++ break++++if end is None:++ raise SystemExit("Could not find COpenVibeJSRuntime::LoadCoreFiles closing brace")++++replacement = '''bool COpenVibeJSRuntime::LoadCoreFiles()++{++ if (!LoadFile("js/core/hook.js"))++ return false;++++ if (!LoadFile("js/core/gamemode.js"))++ return false;++++ if (!LoadFile("js/bridge.js"))++ return false;++++ if (!LoadFile("js/core/command.js"))++ return false;++++ if (!LoadFile("js/core/timer.js"))++ return false;++++ return true;++}'''++++s = s[:idx] + replacement + s[end:]++p.write_text(s)++print("[openvibe] LoadCoreFiles replaced")++PY++++echo "[openvibe] writing Node JS smoke test"++++cat > tools/smoke-js-core-node.mjs <<'JS'++import fs from "node:fs";++import vm from "node:vm";++++let fakeTime = 0;++const messages = [];++++function player() {++ return {++ userId: 1,++ entIndex: 1,++ steamId: () => "STEAM_0:1:1",++ name: () => "SmokePlayer",++ health: () => 87,++ setHealth: () => undefined,++ chat: (msg) => messages.push(`[chat] ${msg}`),++ team: () => 0,++ setTeam: () => undefined,++ runCommand: (cmd) => messages.push(`[runCommand] ${cmd}`)++ };++}++++const context = vm.createContext({++ console,++ globalThis: {},++ OV: {++ log: (msg) => messages.push(`[log] ${msg}`),++ warn: (msg) => messages.push(`[warn] ${msg}`),++ error: (msg) => messages.push(`[error] ${msg}`),++ getMode: () => "hub",++ getMapName: () => "ov_hub",++ time: () => fakeTime,++ broadcast: (msg) => messages.push(`[broadcast] ${msg}`),++ players: () => [player()],++ playerByUserId: () => player(),++ serverCommand: (cmd) => messages.push(`[serverCommand] ${cmd}`),++ reward: () => undefined,++ endMatch: () => undefined++ }++});++context.globalThis = context;++++const files = [++ "game/openvibe.games/js/core/hook.js",++ "game/openvibe.games/js/core/gamemode.js",++ "game/openvibe.games/js/bridge.js",++ "game/openvibe.games/js/core/command.js",++ "game/openvibe.games/js/core/timer.js",++ "game/openvibe.games/js/gamemodes/base/server.js",++ "game/openvibe.games/js/gamemodes/hub/server.js"++];++++for (const file of files) {++ const code = fs.readFileSync(file, "utf8");++ vm.runInContext(code, context, { filename: file });++}++++context.gamemode.call("Initialize");++context.gamemode.call("MapInitialize", "ov_hub");++context.gamemode.call("PlayerInitialSpawn", player());++++const blocked = context.gamemode.call("PlayerSay", player(), "!js");++if (blocked !== false) throw new Error("!js did not block default chat");++++context.gamemode.call("ConsoleCommand", "smoke");++context.gamemode.call("ConsoleCommand", "hub_status");++context.gamemode.call("ConsoleCommand", "timer_smoke");++fakeTime += 1;++context.gamemode.call("Think");++++if (!messages.some((line) => line.includes("JavaScript hooks are working"))) throw new Error("missing !js response");++if (!messages.some((line) => line.includes("OpenVibe embedded JS smoke test passed"))) throw new Error("missing smoke response");++if (!messages.some((line) => line.includes("OpenVibe JS timer smoke fired"))) throw new Error("missing timer smoke response");++++console.log(messages.join("\n"));++console.log("[openvibe-smoke] JS command/timer smoke passed");++JS++++echo "[openvibe] run JS smoke test"++node tools/smoke-js-core-node.mjs++++echo "[openvibe] apply SDK patch"++tools/apply-openvibe-sdk.sh++++echo "[openvibe] build SDK"++if [[ "${RUN_BUILD:-1}" = "1" ]]; then++ tools/build-sdk-linux.sh 2>&1 | tee "$HOME/ov-build.log"++fi++++echo "[openvibe] setup OpenVibe bin"++tools/setup-openvibe-bin.sh++++echo++++echo "[openvibe] JS command/timer phase complete."++echo++++echo "Runtime test:"++echo " OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh"++echo++echo "Server console tests:"++echo " ov_js_status"++echo " ov_js_cmd help"++echo " ov_js_cmd smoke"++echo " ov_js_cmd timer_smoke"++echo " ov_js_cmd hub_status"++echo++echo "Client tests:"++echo " connect 127.0.0.1:27015"++echo " say !help"++echo " say !js"++echo " say !hp"++echo " say !players"++echo " say !where"nnew file mode 100755@@@ -0,0 +1,665 @@++#!/usr/bin/env bash++set -euo pipefail++++ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"++SDK="${OPENVIBE_SDK:-$ROOT/engine/source-sdk-2013}"++STAMP="$(date +%Y%m%d-%H%M%S)"++RUN_BUILD="${RUN_BUILD:-1}"++++cd "$ROOT"++++echo "[openvibe] next phase: JS command registry + timers + smoke tests"++echo "[openvibe] root=$ROOT"++echo "[openvibe] sdk=$SDK"++++backup_file() {++ local file="$1"++ [[ -f "$file" ]] && cp "$file" "$file.bak.$STAMP"++}++++need_file() {++ [[ -f "$1" ]] || { echo "[openvibe] missing required file: $1" >&2; exit 1; }++}++++need_file sdk/openvibe/shared/ov_js_runtime.cpp++need_file tools/apply-openvibe-sdk.sh++need_file tools/build-sdk-linux.sh++need_file tools/setup-openvibe-bin.sh++++mkdir -p game/openvibe.games/js/core game/openvibe.games/js/gamemodes/{base,hub,prophunt,deathrun,fortwars,traitortown} tools++++echo "[openvibe] writing JS core command registry"++++cat > game/openvibe.games/js/core/command.js <<'JS_COMMAND'++(function () {++ const chatCommands = new Map();++ const consoleCommands = new Map();++++ function splitWords(text) {++ const source = String(text || "").trim();++ if (!source) return [];++ return source.split(/\s+/g);++ }++++ function normalizeChatName(name) {++ return String(name || "").replace(/^!+/, "").toLowerCase();++ }++++ function normalizeConsoleName(name) {++ return String(name || "").toLowerCase();++ }++++ function register(map, name, fn, help) {++ if (typeof fn !== "function") throw new Error("command callback must be function");++ const key = map === chatCommands ? normalizeChatName(name) : normalizeConsoleName(name);++ if (!key) throw new Error("command name is required");++ map.set(key, { fn, help: help ? String(help) : "" });++ }++++ function runChat(ply, text) {++ const words = splitWords(text);++ if (words.length === 0) return undefined;++ if (words[0][0] !== "!") return undefined;++++ const name = normalizeChatName(words[0]);++ const entry = chatCommands.get(name);++ if (!entry) return undefined;++++ try {++ const args = words.slice(1);++ const result = entry.fn(ply, args, String(text || ""));++ return result === undefined ? false : result;++ } catch (err) {++ OV.error(`[chat:${name}] ${err && err.stack ? err.stack : err}`);++ if (ply && typeof ply.chat === "function") ply.chat("Command failed. Check server console.");++ return false;++ }++ }++++ function runConsole(text) {++ const words = splitWords(text);++ if (words.length === 0) return undefined;++++ const name = normalizeConsoleName(words[0]);++ const entry = consoleCommands.get(name);++ if (!entry) return undefined;++++ try {++ const args = words.slice(1);++ const result = entry.fn(args, String(text || ""));++ return result === undefined ? false : result;++ } catch (err) {++ OV.error(`[console:${name}] ${err && err.stack ? err.stack : err}`);++ return false;++ }++ }++++ function list(map) {++ const out = [];++ for (const [name, entry] of map.entries()) out.push({ name, help: entry.help });++ out.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);++ return out;++ }++++ globalThis.command = {++ chat(name, fn, help) { register(chatCommands, name, fn, help); },++ console(name, fn, help) { register(consoleCommands, name, fn, help); },++ removeChat(name) { return chatCommands.delete(normalizeChatName(name)); },++ removeConsole(name) { return consoleCommands.delete(normalizeConsoleName(name)); },++ runChat,++ runConsole,++ chatList() { return list(chatCommands); },++ consoleList() { return list(consoleCommands); }++ };++++ globalThis.concommand = {++ add(name, fn, help) { register(consoleCommands, name, fn, help); },++ remove(name) { return consoleCommands.delete(normalizeConsoleName(name)); },++ list() { return list(consoleCommands); }++ };++++ hook.add("PlayerSay", "openvibe.command.chat", runChat);++ hook.add("ConsoleCommand", "openvibe.command.console", runConsole);++++ OV.log("command.js loaded");++})();++JS_COMMAND++++echo "[openvibe] writing JS timer library"++++cat > game/openvibe.games/js/core/timer.js <<'JS_TIMER'++(function () {++ const timers = new Map();++ let nextId = 1;++++ function now() {++ return Number(OV.time() || 0);++ }++++ function create(id, delay, reps, fn) {++ if (typeof fn !== "function") throw new Error("timer callback must be function");++++ const key = String(id || `timer_${nextId++}`);++ const seconds = Math.max(0, Number(delay || 0));++ const repeatCount = reps === undefined ? 1 : Number(reps);++++ timers.set(key, {++ id: key,++ delay: seconds,++ reps: repeatCount,++ runs: 0,++ next: now() + seconds,++ fn++ });++++ return key;++ }++++ function simple(delay, fn) {++ return create(null, delay, 1, fn);++ }++++ function remove(id) {++ return timers.delete(String(id));++ }++++ function exists(id) {++ return timers.has(String(id));++ }++++ function tick() {++ const t = now();++ const due = [];++++ for (const item of timers.values()) {++ if (t >= item.next) due.push(item.id);++ }++++ for (const id of due) {++ const item = timers.get(id);++ if (!item) continue;++++ try {++ item.runs += 1;++ item.fn(item.runs, id);++ } catch (err) {++ OV.error(`[timer:${id}] ${err && err.stack ? err.stack : err}`);++ }++++ if (item.reps > 0 && item.runs >= item.reps) {++ timers.delete(id);++ } else {++ item.next = now() + item.delay;++ }++ }++++ return undefined;++ }++++ function list() {++ const out = [];++ const t = now();++ for (const item of timers.values()) {++ out.push({++ id: item.id,++ delay: item.delay,++ reps: item.reps,++ runs: item.runs,++ remaining: Math.max(0, item.next - t)++ });++ }++ return out;++ }++++ globalThis.timer = { create, simple, remove, exists, list };++ hook.add("Think", "openvibe.timer.tick", tick);++++ OV.log("timer.js loaded");++})();++JS_TIMER++++echo "[openvibe] patching runtime load order for command.js and timer.js"++++backup_file sdk/openvibe/shared/ov_js_runtime.cpp++python3 <<'PY'++from pathlib import Path++++p = Path("sdk/openvibe/shared/ov_js_runtime.cpp")++s = p.read_text()++old = '''bool COpenVibeJSRuntime::LoadCoreFiles()++{++ if (!LoadFile("js/core/hook.js"))++ return false;++++ if (!LoadFile("js/core/gamemode.js"))++ return false;++++ if (!LoadFile("js/bridge.js"))++ return false;++++ return true;++}++'''++new = '''bool COpenVibeJSRuntime::LoadCoreFiles()++{++ if (!LoadFile("js/core/hook.js"))++ return false;++++ if (!LoadFile("js/core/gamemode.js"))++ return false;++++ if (!LoadFile("js/bridge.js"))++ return false;++++ if (!LoadFile("js/core/command.js"))++ return false;++++ if (!LoadFile("js/core/timer.js"))++ return false;++++ return true;++}++'''++if old not in s:++ start = s.find("bool COpenVibeJSRuntime::LoadCoreFiles()")++ if start == -1:++ raise SystemExit("LoadCoreFiles not found")++ brace = s.find("{", start)++ depth = 0++ end = None++ for i in range(brace, len(s)):++ if s[i] == "{":++ depth += 1++ elif s[i] == "}":++ depth -= 1++ if depth == 0:++ end = i + 1++ break++ if end is None:++ raise SystemExit("LoadCoreFiles end not found")++ s = s[:start] + new + s[end:]++else:++ s = s.replace(old, new)++p.write_text(s)++PY++++echo "[openvibe] writing base gamemode with command/timer smoke support"++++cat > game/openvibe.games/js/gamemodes/base/server.js <<'JS_BASE'++const GM = {++ mode: "base",++ name: "OpenVibe Base",++++ Initialize() {++ OV.log("Base Initialize fired");++++ command.console("help", () => {++ const chats = command.chatList().map((cmd) => `!${cmd.name}`).join(", ");++ const consoles = command.consoleList().map((cmd) => cmd.name).join(", ");++ OV.log(`Chat commands: ${chats || "none"}`);++ OV.log(`Console commands: ${consoles || "none"}`);++ return false;++ }, "List OpenVibe JS commands");++++ command.console("smoke", () => {++ OV.log("OpenVibe embedded JS smoke command passed.");++ OV.broadcast("OpenVibe embedded JS smoke command passed.");++ return false;++ }, "Run embedded JS smoke test");++++ command.console("timer_smoke", () => {++ OV.log("Scheduling timer smoke test.");++ timer.simple(1.0, () => OV.broadcast("OpenVibe JS timer smoke passed."));++ return false;++ }, "Schedule a one-second timer smoke test");++++ command.chat("help", (ply) => {++ const names = command.chatList().map((cmd) => `!${cmd.name}`).join(", ");++ ply.chat(`Commands: ${names || "none"}`);++ return false;++ }, "List chat commands");++++ command.chat("js", (ply) => {++ ply.chat("JavaScript command registry is working.");++ return false;++ }, "JS smoke test");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source.");++ },++++ PlayerSpawn(_ply) {},++++ PlayerDeath(_victim, _attacker) {},++++ PlayerDisconnected(_ply) {},++++ PlayerSay(_ply, _text) {++ return undefined;++ },++++ ConsoleCommand(text) {++ OV.log(`Unhandled ConsoleCommand: ${text}`);++ return undefined;++ },++++ Think() {}++};++++gamemode.set(GM);++JS_BASE++++echo "[openvibe] writing hub gamemode using command registry"++++cat > game/openvibe.games/js/gamemodes/hub/server.js <<'JS_HUB'++const GM = {++ mode: "hub",++ name: "OpenVibe Hub",++++ Initialize() {++ OV.log("Hub Initialize fired");++++ command.chat("hp", (ply) => {++ ply.chat(`Health: ${ply.health()}`);++ return false;++ }, "Show current health");++++ command.chat("players", (ply) => {++ ply.chat(`Players online: ${OV.players().length}`);++ return false;++ }, "Show player count");++++ command.chat("where", (ply) => {++ ply.chat(`Mode=${OV.getMode()} Map=${OV.getMapName()}`);++ return false;++ }, "Show mode and map");++++ command.console("hub_status", () => {++ OV.log(`Hub status: mode=${OV.getMode()} map=${OV.getMapName()} players=${OV.players().length}`);++ return false;++ }, "Print hub status");++ },++++ MapInitialize(mapName) {++ OV.log(`Map initialized: ${mapName}`);++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Welcome to OpenVibe: Source JS runtime.");++ ply.chat("Type !help for JS commands.");++ OV.broadcast(`${ply.name()} joined the hub.`);++ },++++ PlayerSpawn(ply) {++ timer.simple(0.2, () => ply.chat("PlayerSpawn hook fired."));++ },++++ PlayerSay(_ply, _text) {++ return undefined;++ },++++ ConsoleCommand(text) {++ OV.log(`Hub unhandled ConsoleCommand: ${text}`);++ return undefined;++ },++++ Think() {}++};++++gamemode.set(GM);++JS_HUB++++echo "[openvibe] updating prototype gamemodes to use command registry when available"++++cat > game/openvibe.games/js/gamemodes/prophunt/server.js <<'JS_PH'++const props = ["can", "crate", "barrel", "chair", "bucket"];++++function randomProp() {++ return props[Math.floor(Math.random() * props.length)];++}++++const GM = {++ mode: "prophunt",++ name: "OpenVibe Prop Hunt",++++ Initialize() {++ OV.log("Prop Hunt Initialize fired");++++ command.chat("prop", (ply, args) => {++ const prop = args[0] || randomProp();++ ply.runCommand(`ov_prophunt_disguise ${prop}`);++ return false;++ }, "Disguise as a prop");++++ command.chat("human", (ply) => {++ ply.runCommand("ov_prophunt_reset_disguise");++ return false;++ }, "Reset disguise");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Prop Hunt: use !prop or !prop crate. Use !human to reset.");++ },++++ Think() {}++};++++gamemode.set(GM);++JS_PH++++cat > game/openvibe.games/js/gamemodes/fortwars/server.js <<'JS_FW'++const allowed = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);++++const GM = {++ mode: "fortwars",++ name: "OpenVibe Fort Wars",++++ Initialize() {++ OV.log("Fort Wars Initialize fired");++++ command.chat("build", (ply, args) => {++ const part = args[0] || "";++ if (!allowed.has(part)) {++ ply.chat("Allowed: crate, barrel, pallet, fence, sheet");++ return false;++ }++++ ply.runCommand(`ov_fortwars_spawn ${part}`);++ return false;++ }, "Spawn a Fort Wars build prop");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Fort Wars: build first, fight second. Try !build crate.");++ }++};++++gamemode.set(GM);++JS_FW++++cat > game/openvibe.games/js/gamemodes/deathrun/server.js <<'JS_DR'++const GM = {++ mode: "deathrun",++ name: "OpenVibe Deathrun",++++ Initialize() {++ OV.log("Deathrun Initialize fired");++++ command.chat("finish", (ply) => {++ ply.chat("Deathrun finish test.");++ OV.reward(ply, 50, 100, "deathrun_finish");++ return false;++ }, "Smoke-test Deathrun finish reward path");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Deathrun: survive the traps and reach the finish.");++ }++};++++gamemode.set(GM);++JS_DR++++cat > game/openvibe.games/js/gamemodes/traitortown/server.js <<'JS_TT'++const roles = new Map();++++const GM = {++ mode: "traitortown",++ name: "OpenVibe Traitor Town",++++ Initialize() {++ OV.log("Traitor Town Initialize fired");++++ command.chat("role", (ply) => {++ ply.chat(`Your role: ${roles.get(ply.userId()) || "innocent"}`);++ return false;++ }, "Show your role");++ },++++ PlayerInitialSpawn(ply) {++ ply.chat("Traitor Town: find the traitors before they find you.");++ roles.set(ply.userId(), "innocent");++ },++++ PlayerDeath(victim, _attacker) {++ OV.broadcast(`${victim.name()} died.`);++ }++};++++gamemode.set(GM);++JS_TT++++echo "[openvibe] writing Node-based JS core smoke test"++++cat > tools/smoke-js-core-node.mjs <<'NODE_SMOKE'++import fs from "node:fs";++import vm from "node:vm";++++const files = [++ "game/openvibe.games/js/core/hook.js",++ "game/openvibe.games/js/core/gamemode.js",++ "game/openvibe.games/js/bridge.js",++ "game/openvibe.games/js/core/command.js",++ "game/openvibe.games/js/core/timer.js",++ "game/openvibe.games/js/gamemodes/base/server.js",++ "game/openvibe.games/js/gamemodes/hub/server.js"++];++++const messages = [];++let fakeTime = 0;++++const context = vm.createContext({++ console,++ Map,++ Set,++ Math,++ Number,++ String,++ Array,++ Object,++ Error,++ globalThis: {},++ OV: {++ log: (msg) => messages.push(`[log] ${msg}`),++ warn: (msg) => messages.push(`[warn] ${msg}`),++ error: (msg) => messages.push(`[error] ${msg}`),++ getMode: () => "hub",++ getMapName: () => "ov_hub",++ time: () => fakeTime,++ broadcast: (msg) => messages.push(`[broadcast] ${msg}`),++ players: () => [],++ playerByUserId: () => null,++ serverCommand: (cmd) => messages.push(`[serverCommand] ${cmd}`),++ fireHook: () => undefined,++ reward: () => undefined,++ endMatch: () => undefined++ }++});++context.globalThis = context;++++for (const file of files) {++ const code = fs.readFileSync(file, "utf8");++ vm.runInContext(code, context, { filename: file });++}++++context.gamemode.call("Initialize");++context.gamemode.call("MapInitialize", "ov_hub");++context.gamemode.call("ConsoleCommand", "smoke");++context.gamemode.call("ConsoleCommand", "timer_smoke");++fakeTime = 2;++context.gamemode.call("Think");++++const fakePlayer = {++ userId: () => 1,++ entIndex: () => 1,++ steamId: () => "STEAM_0:1:1",++ name: () => "SmokePlayer",++ health: () => 100,++ setHealth: () => {},++ chat: (msg) => messages.push(`[chat] ${msg}`),++ runCommand: (cmd) => messages.push(`[runCommand] ${cmd}`)++};++context.gamemode.call("PlayerSay", fakePlayer, "!js");++context.gamemode.call("PlayerSay", fakePlayer, "!hp");++context.gamemode.call("PlayerSay", fakePlayer, "!players");++++const required = [++ "command.js loaded",++ "timer.js loaded",++ "Hub Initialize fired",++ "OpenVibe embedded JS smoke command passed.",++ "OpenVibe JS timer smoke passed.",++ "JavaScript command registry is working.",++ "Health: 100",++ "Players online: 0"++];++++for (const needle of required) {++ if (!messages.some((line) => line.includes(needle))) {++ console.error(messages.join("\n"));++ throw new Error(`missing smoke output: ${needle}`);++ }++}++++console.log("[openvibe] JS core smoke passed");++for (const line of messages) console.log(line);++NODE_SMOKE++chmod +x tools/smoke-js-core-node.mjs++++echo "[openvibe] run JS core smoke test"++node tools/smoke-js-core-node.mjs++++echo "[openvibe] apply SDK patch"++tools/apply-openvibe-sdk.sh++++if [[ "$RUN_BUILD" = "1" ]]; then++ echo "[openvibe] build SDK"++ tools/build-sdk-linux.sh 2>&1 | tee "$HOME/ov-build.log"++++ echo "[openvibe] setup OpenVibe bin"++ tools/setup-openvibe-bin.sh++++ echo++ echo "[openvibe] next phase build complete."++ echo++ echo "Runtime test:"++ echo " OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh"++ echo++ echo "Server console tests:"++ echo " ov_js_status"++ echo " ov_js_cmd help"++ echo " ov_js_cmd smoke"++ echo " ov_js_cmd timer_smoke"++ echo " ov_js_cmd hub_status"++ echo++ echo "Client tests:"++ echo " connect 127.0.0.1:27015"++ echo " say !help"++ echo " say !js"++ echo " say !hp"++ echo " say !players"++ echo " say !where"++else++ echo "[openvibe] skipped SDK build because RUN_BUILD=0"++finnew file mode 100755@@@ -0,0 +1,75 @@++import fs from "node:fs";++import vm from "node:vm";++++let fakeTime = 0;++const messages = [];++++function player() {++ return {++ userId: 1,++ entIndex: 1,++ steamId: () => "STEAM_0:1:1",++ name: () => "SmokePlayer",++ health: () => 87,++ setHealth: () => undefined,++ chat: (msg) => messages.push(`[chat] ${msg}`),++ team: () => 0,++ setTeam: () => undefined,++ runCommand: (cmd) => messages.push(`[runCommand] ${cmd}`)++ };++}++++const context = vm.createContext({++ console,++ globalThis: {},++ OV: {++ log: (msg) => messages.push(`[log] ${msg}`),++ warn: (msg) => messages.push(`[warn] ${msg}`),++ error: (msg) => messages.push(`[error] ${msg}`),++ getMode: () => "hub",++ getMapName: () => "ov_hub",++ time: () => fakeTime,++ broadcast: (msg) => messages.push(`[broadcast] ${msg}`),++ players: () => [player()],++ playerByUserId: () => player(),++ serverCommand: (cmd) => messages.push(`[serverCommand] ${cmd}`),++ reward: () => undefined,++ endMatch: () => undefined++ }++});++context.globalThis = context;++++const files = [++ "game/openvibe.games/js/core/hook.js",++ "game/openvibe.games/js/core/gamemode.js",++ "game/openvibe.games/js/bridge.js",++ "game/openvibe.games/js/core/command.js",++ "game/openvibe.games/js/core/timer.js",++ "game/openvibe.games/js/gamemodes/base/server.js",++ "game/openvibe.games/js/gamemodes/hub/server.js"++];++++for (const file of files) {++ const code = fs.readFileSync(file, "utf8");++ vm.runInContext(code, context, { filename: file });++}++++context.gamemode.call("Initialize");++context.gamemode.call("MapInitialize", "ov_hub");++context.gamemode.call("PlayerInitialSpawn", player());++++const blocked = context.gamemode.call("PlayerSay", player(), "!js");++if (blocked !== false) throw new Error("!js did not block default chat");++++context.gamemode.call("ConsoleCommand", "smoke");++context.gamemode.call("ConsoleCommand", "hub_status");++context.gamemode.call("ConsoleCommand", "timer_smoke");++fakeTime += 1;++context.gamemode.call("Think");++++if (!messages.some((line) => line.includes("JavaScript hooks are working"))) throw new Error("missing !js response");++if (!messages.some((line) => line.includes("OpenVibe embedded JS smoke test passed"))) throw new Error("missing smoke response");++if (!messages.some((line) => line.includes("OpenVibe JS timer smoke fired"))) throw new Error("missing timer smoke response");++++console.log(messages.join("\n"));++console.log("[openvibe-smoke] JS command/timer smoke passed");
@@ -1,20 +1,5 @@
@@ -1,20 +1,5 @@
-OV.log("bridge.js loaded");
-
-globalThis.console = {
- log: (...args) => OV.log(args.map(String).join(" ")),
- warn: (...args) => OV.warn(args.map(String).join(" ")),
- error: (...args) => OV.error(args.map(String).join(" "))
-};
-
-globalThis.game = {
- mode: () => OV.getMode(),
- map: () => OV.getMapName(),
- time: () => OV.time(),
- broadcast: (msg) => OV.broadcast(String(msg)),
- serverCommand: (cmd) => OV.serverCommand(String(cmd))
-};
-
-globalThis.round = {
- start: () => OV.fireHook("RoundStart"),
- end: () => OV.fireHook("RoundEnd")
-};
+(function () {
+ if (globalThis.OV && typeof OV.log === "function") {
+ OV.log("bridge.js loaded");
+ }
+})();
@@ -1,39 +1,25 @@
@@ -1,39 +1,25 @@
-const GM = {
- mode: "base",
- name: "OpenVibe Base",
-
- Initialize() {
- OV.log("Base Initialize fired");
- },
-
- MapInitialize(mapName) {
- OV.log(`Base MapInitialize: ${mapName}`);
- },
-
- PlayerInitialSpawn(ply) {
- ply.chat("Welcome to OpenVibe: Source.");
- },
-
- PlayerSpawn(_ply) {},
-
- PlayerDeath(_victim, _attacker) {},
-
- PlayerDisconnected(_ply) {},
-
- PlayerSay(_ply, _text) {
- return undefined;
- },
-
- ConsoleCommand(text) {
- OV.log(`ConsoleCommand: ${text}`);
- return undefined;
- },
-
- Think() {},
-
-ConsoleCommand(text) {
- OV.log(`Base ConsoleCommand: ${text}`);
- }
-};
-
-gamemode.set(GM);
+(function () {
+ const BaseServerGM = {
+ mode: "base",
+ name: "OpenVibe Base",
+
+ Initialize() {
+ OV.log("Base Initialize fired");
+ },
+
+ MapInitialize(mapName) {
+ OV.log(`Base MapInitialize: ${mapName}`);
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source.");
+ },
+
+ PlayerSpawn(_ply) {},
+ PlayerDeath(_victim, _attacker) {},
+ PlayerDisconnected(_ply) {},
+ Think() {}
+ };
+
+ gamemode.set(BaseServerGM);
+})();
@@ -1,24 +1,18 @@
@@ -1,24 +1,18 @@
-const GM = {
- mode: "deathrun",
- name: "OpenVibe Deathrun",
-
- Initialize() {
- OV.log("Deathrun Initialize fired");
- },
-
- PlayerInitialSpawn(ply) {
- ply.chat("Deathrun: survive the traps and reach the finish.");
- },
-
- PlayerSay(ply, text) {
- if (text === "!finish") {
- ply.chat("Deathrun finish test.");
- OV.reward(ply, 50, 100, "deathrun_finish");
+(function () {
+ function registerCommands() {
+ if (!globalThis.command) return;
+ command.add("dr_status", "Show Deathrun status", function ({ ply, reply }) {
+ reply(ply, `Deathrun JS online. map=${OV.getMapName()} players=${OV.players().length}`);
return false;
return false;
- }
-
- return undefined;
+ });
}
}
-};
-gamemode.set(GM);
+ const DeathrunGM = {
+ mode: "deathrun",
+ name: "OpenVibe Deathrun",
+ Initialize() { OV.log("Deathrun Initialize fired"); registerCommands(); },
+ PlayerInitialSpawn(ply) { ply.chat("Deathrun JS loaded. Try !dr_status"); },
+ Think() {}
+ };
+ gamemode.set(DeathrunGM);
+})();
@@ -1,29 +1,25 @@
@@ -1,29 +1,25 @@
-const allowed = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);
-
-const GM = {
- mode: "fortwars",
- name: "OpenVibe Fort Wars",
-
- Initialize() {
- OV.log("Fort Wars Initialize fired");
- },
-
- PlayerInitialSpawn(ply) {
- ply.chat("Fort Wars: build first, fight second.");
- },
-
- PlayerSay(ply, text) {
- if (!text.startsWith("!build ")) return undefined;
-
- const part = text.slice(7).trim();
- if (!allowed.has(part)) {
- ply.chat("Allowed: crate, barrel, pallet, fence, sheet");
+(function () {
+ function registerCommands() {
+ if (!globalThis.command) return;
+ command.add("fw_status", "Show Fort Wars status", function ({ ply, reply }) {
+ reply(ply, `Fort Wars JS online. map=${OV.getMapName()} players=${OV.players().length}`);
return false;
return false;
- }
-
- ply.runCommand(`ov_fortwars_spawn ${part}`);
- return false;
+ });
+ command.add("build", "Spawn an allowlisted Fort Wars prop", function ({ args, ply, reply }) {
+ const choice = args[0] || "crate";
+ if (!ply) return false;
+ ply.runCommand(`ov_fortwars_spawn ${choice}`);
+ reply(ply, `Trying Fort Wars prop: ${choice}`);
+ return false;
+ });
}
}
-};
-gamemode.set(GM);
+ const FortWarsGM = {
+ mode: "fortwars",
+ name: "OpenVibe Fort Wars",
+ Initialize() { OV.log("Fort Wars Initialize fired"); registerCommands(); },
+ PlayerInitialSpawn(ply) { ply.chat("Fort Wars JS loaded. Try !fw_status or !build crate"); },
+ Think() {}
+ };
+ gamemode.set(FortWarsGM);
+})();
@@ -1,55 +1,67 @@
@@ -1,55 +1,67 @@
-const GM = {
- mode: "hub",
- name: "OpenVibe Hub",
-
- Initialize() {
- OV.log("Hub Initialize fired");
- },
-
- MapInitialize(mapName) {
- OV.log(`Map initialized: ${mapName}`);
- },
-
- PlayerInitialSpawn(ply) {
- ply.chat("Welcome to OpenVibe: Source JS runtime.");
- OV.broadcast(`${ply.name()} joined the hub.`);
- },
-
- PlayerSpawn(ply) {
- ply.chat("PlayerSpawn hook fired.");
- },
-
- PlayerSay(ply, text) {
- if (text === "!js") {
- ply.chat("JavaScript hooks are working.");
- return false;
- }
+(function () {
+ function registerHubCommands() {
+ if (!globalThis.command) return;
- if (text === "!hp") {
- ply.chat(`Health: ${ply.health()}`);
+ command.add("js", "Confirm JavaScript hooks are working", function ({ ply, reply }) {
+ reply(ply, "JavaScript hooks are working.");
return false;
return false;
- }
+ });
- if (text === "!players") {
- ply.chat(`Players online: ${OV.players().length}`);
+ command.add("hp", "Show current health", function ({ ply, reply }) {
+ if (!ply) return false;
+ reply(ply, `Health: ${ply.health()}`);
return false;
return false;
- }
+ });
- return undefined;
- },
+ command.add("players", "Show player count", function ({ ply, reply }) {
+ reply(ply, `Players online: ${OV.players().length}`);
+ return false;
+ });
- ConsoleCommand(text) {
- OV.log(`Hub ConsoleCommand: ${text}`);
+ command.add("where", "Show current mode and map", function ({ ply, reply }) {
+ reply(ply, `Mode=${OV.getMode()} map=${OV.getMapName()}`);
+ return false;
+ });
- if (text === "smoke") {
- OV.broadcast("OpenVibe embedded JS smoke test passed.");
+ command.add("hub_status", "Broadcast hub status", function ({ ply, reply }) {
+ reply(ply, `Hub OK. mode=${OV.getMode()} map=${OV.getMapName()} players=${OV.players().length}`);
return false;
return false;
- }
+ });
+ }
+
+ const HubServerGM = {
+ mode: "hub",
+ name: "OpenVibe Hub",
+
+ Initialize() {
+ OV.log("Hub Initialize fired");
+ registerHubCommands();
+ },
+
+ MapInitialize(mapName) {
+ OV.log(`Hub MapInitialize: ${mapName}`);
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source JS runtime.");
+ OV.broadcast(`${ply.name()} joined the hub.`);
+ },
+
+ PlayerSpawn(ply) {
+ ply.chat("PlayerSpawn hook fired.");
+ },
+
+ PlayerSay(_ply, _text) {
+ return undefined;
+ },
- return undefined;
- },
+ ConsoleCommand(text) {
+ OV.log(`Hub ConsoleCommand fallback: ${text}`);
+ return undefined;
+ },
- Think() {}
-};
+ Think() {}
+ };
-gamemode.set(GM);
+ gamemode.set(HubServerGM);
+})();
@@ -1,36 +1,25 @@
@@ -1,36 +1,25 @@
-const props = ["crate", "barrel", "chair", "bucket"];
-
-function randomProp() {
- return props[Math.floor(Math.random() * props.length)];
-}
-
-const GM = {
- mode: "prophunt",
- name: "OpenVibe Prop Hunt",
-
- Initialize() {
- OV.log("Prop Hunt Initialize fired");
- },
-
- PlayerInitialSpawn(ply) {
- ply.chat("Prop Hunt: hide as props or hunt them down.");
- },
-
- PlayerSay(ply, text) {
- if (text === "!prop") {
- ply.runCommand(`ov_prophunt_disguise ${randomProp()}`);
+(function () {
+ function registerCommands() {
+ if (!globalThis.command) return;
+ command.add("ph_status", "Show Prop Hunt status", function ({ ply, reply }) {
+ reply(ply, `Prop Hunt JS online. map=${OV.getMapName()} players=${OV.players().length}`);
return false;
return false;
- }
-
- if (text.startsWith("!prop ")) {
- ply.runCommand(`ov_prophunt_disguise ${text.slice(6).trim()}`);
+ });
+ command.add("disguise", "Disguise as an allowlisted prop", function ({ args, ply, reply }) {
+ const choice = args[0] || "crate";
+ if (!ply) return false;
+ ply.runCommand(`ov_prophunt_disguise ${choice}`);
+ reply(ply, `Trying prop disguise: ${choice}`);
return false;
return false;
- }
-
- return undefined;
- },
-
- Think() {}
-};
-
-gamemode.set(GM);
+ });
+ }
+
+ const PropHuntGM = {
+ mode: "prophunt",
+ name: "OpenVibe Prop Hunt",
+ Initialize() { OV.log("Prop Hunt Initialize fired"); registerCommands(); },
+ PlayerInitialSpawn(ply) { ply.chat("Prop Hunt JS loaded. Try !ph_status or !disguise crate"); },
+ Think() {}
+ };
+ gamemode.set(PropHuntGM);
+})();
@@ -1,30 +1,18 @@
@@ -1,30 +1,18 @@
-const roles = new Map();
-
-const GM = {
- mode: "traitortown",
- name: "OpenVibe Traitor Town",
-
- Initialize() {
- OV.log("Traitor Town Initialize fired");
- },
-
- PlayerInitialSpawn(ply) {
- ply.chat("Traitor Town: find the traitors before they find you.");
- roles.set(ply.userId(), "innocent");
- },
-
- PlayerSay(ply, text) {
- if (text === "!role") {
- ply.chat(`Your role: ${roles.get(ply.userId()) || "none"}`);
+(function () {
+ function registerCommands() {
+ if (!globalThis.command) return;
+ command.add("ttt_status", "Show Traitor Town status", function ({ ply, reply }) {
+ reply(ply, `Traitor Town JS online. map=${OV.getMapName()} players=${OV.players().length}`);
return false;
return false;
- }
-
- return undefined;
- },
-
- PlayerDeath(victim, attacker) {
- OV.broadcast(`${victim.name()} died.`);
+ });
}
}
-};
-gamemode.set(GM);
+ const TraitorTownGM = {
+ mode: "traitortown",
+ name: "OpenVibe Traitor Town",
+ Initialize() { OV.log("Traitor Town Initialize fired"); registerCommands(); },
+ PlayerInitialSpawn(ply) { ply.chat("Traitor Town JS loaded. Try !ttt_status"); },
+ Think() {}
+ };
+ gamemode.set(TraitorTownGM);
+})();
@@ -128,6 +128,12 @@ bool COpenVibeJSRuntime::LoadCoreFiles()
@@ -128,6 +128,12 @@ bool COpenVibeJSRuntime::LoadCoreFiles()
if (!LoadFile("js/bridge.js"))
if (!LoadFile("js/bridge.js"))
return false;
return false;
+ if (!LoadFile("js/core/command.js"))
+ return false;
+
+ if (!LoadFile("js/core/timer.js"))
+ return false;
+
return true;
return true;
}
}
new file mode 100755
new file mode 100755
@@ -0,0 +1,713 @@
@@ -0,0 +1,713 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+SDK="${OPENVIBE_SDK:-$ROOT/engine/source-sdk-2013}"
+STAMP="$(date +%Y%m%d-%H%M%S)"
+
+cd "$ROOT"
+
+echo "[openvibe] fix JS global-scope redeclare and continue command/timer phase"
+echo "[openvibe] root=$ROOT"
+echo "[openvibe] sdk=$SDK"
+
+backup_file() {
+ local file="$1"
+ [[ -f "$file" ]] && cp "$file" "$file.bak.$STAMP"
+}
+
+mkdir -p \
+ game/openvibe.games/js/core \
+ game/openvibe.games/js/gamemodes/base \
+ game/openvibe.games/js/gamemodes/hub \
+ game/openvibe.games/js/gamemodes/prophunt \
+ game/openvibe.games/js/gamemodes/deathrun \
+ game/openvibe.games/js/gamemodes/fortwars \
+ game/openvibe.games/js/gamemodes/traitortown \
+ tools
+
+echo "[openvibe] writing canonical command.js"
+backup_file game/openvibe.games/js/core/command.js
+cat > game/openvibe.games/js/core/command.js <<'JS'
+(function () {
+ const commands = new Map();
+
+ function say(target, message) {
+ const text = String(message);
+ if (target && typeof target.chat === "function") {
+ target.chat(text);
+ } else {
+ OV.log(text);
+ }
+ }
+
+ function normalizeName(name) {
+ return String(name || "").trim().replace(/^[!/]+/, "").toLowerCase();
+ }
+
+ function parse(text) {
+ const raw = String(text || "").trim();
+ const withoutPrefix = raw.replace(/^[!/]+/, "");
+ const parts = withoutPrefix.length ? withoutPrefix.split(/\s+/) : [];
+ const name = normalizeName(parts.shift() || "");
+ return { raw, name, args: parts };
+ }
+
+ globalThis.command = {
+ add(name, descriptionOrFn, maybeFn) {
+ const key = normalizeName(name);
+ if (!key) throw new Error("command.add requires command name");
+
+ let description = "";
+ let fn = maybeFn;
+
+ if (typeof descriptionOrFn === "function") {
+ fn = descriptionOrFn;
+ } else {
+ description = String(descriptionOrFn || "");
+ }
+
+ if (typeof fn !== "function") throw new Error(`command '${key}' requires function`);
+ commands.set(key, { name: key, description, fn });
+ },
+
+ remove(name) {
+ return commands.delete(normalizeName(name));
+ },
+
+ has(name) {
+ return commands.has(normalizeName(name));
+ },
+
+ list() {
+ return Array.from(commands.values()).map((entry) => ({
+ name: entry.name,
+ description: entry.description
+ }));
+ },
+
+ run(source, text, ...extra) {
+ const parsed = parse(text);
+ if (!parsed.name) return undefined;
+
+ const entry = commands.get(parsed.name);
+ if (!entry) {
+ say(source, `Unknown command: ${parsed.name}. Try !help.`);
+ return source ? false : undefined;
+ }
+
+ try {
+ const result = entry.fn(source, parsed.args, parsed.raw, ...extra);
+ return result === undefined ? false : result;
+ } catch (err) {
+ OV.error(`[command:${parsed.name}] ${err && err.stack ? err.stack : err}`);
+ say(source, `Command failed: ${parsed.name}`);
+ return false;
+ }
+ }
+ };
+
+ hook.add("ConsoleCommand", "OpenVibeCommandConsole", function (text) {
+ return command.run(null, text);
+ });
+
+ hook.add("PlayerSay", "OpenVibeCommandChat", function (ply, text) {
+ const msg = String(text || "").trim();
+ if (!msg.startsWith("!")) return undefined;
+ return command.run(ply, msg.slice(1));
+ });
+})();
+JS
+
+echo "[openvibe] writing canonical timer.js"
+backup_file game/openvibe.games/js/core/timer.js
+cat > game/openvibe.games/js/core/timer.js <<'JS'
+(function () {
+ const timers = new Map();
+ let nextId = 1;
+
+ function now() {
+ if (typeof OV !== "undefined" && typeof OV.time === "function") return Number(OV.time()) || 0;
+ return Date.now() / 1000;
+ }
+
+ function makeId() {
+ return `timer_${nextId++}`;
+ }
+
+ globalThis.timer = {
+ create(id, delay, repetitions, fn) {
+ if (typeof id !== "string" || !id) throw new Error("timer.create requires id");
+ if (typeof fn !== "function") throw new Error("timer.create requires callback");
+
+ const safeDelay = Math.max(0, Number(delay) || 0);
+ const reps = repetitions === undefined ? 1 : Number(repetitions);
+
+ timers.set(id, {
+ id,
+ delay: safeDelay,
+ repetitions: reps,
+ fn,
+ next: now() + safeDelay,
+ running: true
+ });
+
+ return id;
+ },
+
+ simple(delay, fn) {
+ return timer.create(makeId(), delay, 1, fn);
+ },
+
+ remove(id) {
+ return timers.delete(String(id));
+ },
+
+ exists(id) {
+ return timers.has(String(id));
+ },
+
+ count() {
+ return timers.size;
+ },
+
+ tick() {
+ const t = now();
+
+ for (const [id, item] of Array.from(timers.entries())) {
+ if (!item.running || t < item.next) continue;
+
+ try {
+ item.fn();
+ } catch (err) {
+ OV.error(`[timer:${id}] ${err && err.stack ? err.stack : err}`);
+ }
+
+ if (item.repetitions > 0) item.repetitions -= 1;
+
+ if (item.repetitions === 0) {
+ timers.delete(id);
+ } else {
+ item.next = t + item.delay;
+ }
+ }
+ },
+
+ clear() {
+ timers.clear();
+ }
+ };
+
+ hook.add("Think", "OpenVibeTimerThink", function () {
+ timer.tick();
+ return undefined;
+ });
+})();
+JS
+
+echo "[openvibe] writing scoped base gamemode"
+backup_file game/openvibe.games/js/gamemodes/base/server.js
+cat > game/openvibe.games/js/gamemodes/base/server.js <<'JS'
+(function () {
+ function say(target, message) {
+ if (target && typeof target.chat === "function") target.chat(String(message));
+ else OV.log(String(message));
+ }
+
+ command.add("help", "List available OpenVibe commands.", function (source) {
+ const names = command.list().map((entry) => `!${entry.name}`).sort().join(", ");
+ say(source, `Commands: ${names}`);
+ return false;
+ });
+
+ command.add("smoke", "Run embedded JS smoke test.", function (source) {
+ const message = "OpenVibe embedded JS smoke test passed.";
+ if (source) say(source, message);
+ OV.broadcast(message);
+ return false;
+ });
+
+ command.add("timer_smoke", "Run timer smoke test.", function (source) {
+ timer.simple(0, function () {
+ OV.broadcast("OpenVibe timer smoke fired.");
+ });
+ timer.tick();
+ say(source, "Timer smoke scheduled.");
+ return false;
+ });
+
+ const GM = {
+ mode: "base",
+ name: "OpenVibe Base",
+
+ Initialize() {
+ OV.log("Base Initialize fired");
+ },
+
+ MapInitialize(mapName) {
+ OV.log(`Base MapInitialize: ${mapName}`);
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source.");
+ },
+
+ PlayerSpawn(_ply) {},
+
+ PlayerDeath(_victim, _attacker) {},
+
+ PlayerDisconnected(_ply) {},
+
+ PlayerSay(ply, text) {
+ const msg = String(text || "").trim();
+ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));
+ return undefined;
+ },
+
+ ConsoleCommand(text) {
+ return command.run(null, text);
+ },
+
+ Think() {
+ timer.tick();
+ }
+ };
+
+ gamemode.set(GM);
+})();
+JS
+
+echo "[openvibe] writing scoped hub gamemode"
+backup_file game/openvibe.games/js/gamemodes/hub/server.js
+cat > game/openvibe.games/js/gamemodes/hub/server.js <<'JS'
+(function () {
+ command.add("hub_status", "Print hub runtime status.", function (source) {
+ const message = `Hub mode online. Map=${OV.getMapName()} Players=${OV.players().length}`;
+ if (source && typeof source.chat === "function") source.chat(message);
+ else OV.log(message);
+ return false;
+ });
+
+ command.add("where", "Show current map/mode.", function (source) {
+ const message = `You are on ${OV.getMapName()} in ${OV.getMode()} mode.`;
+ if (source && typeof source.chat === "function") source.chat(message);
+ else OV.log(message);
+ return false;
+ });
+
+ const GM = {
+ mode: "hub",
+ name: "OpenVibe Hub",
+
+ Initialize() {
+ OV.log("Hub Initialize fired");
+ },
+
+ MapInitialize(mapName) {
+ OV.log(`Map initialized: ${mapName}`);
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source JS runtime.");
+ OV.broadcast(`${ply.name()} joined the hub.`);
+ },
+
+ PlayerSpawn(_ply) {},
+
+ PlayerSay(ply, text) {
+ const msg = String(text || "").trim();
+
+ if (msg === "!js") {
+ ply.chat("JavaScript hooks are working.");
+ return false;
+ }
+
+ if (msg === "!hp") {
+ ply.chat(`Health: ${ply.health()}`);
+ return false;
+ }
+
+ if (msg === "!players") {
+ ply.chat(`Players online: ${OV.players().length}`);
+ return false;
+ }
+
+ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));
+ return undefined;
+ },
+
+ ConsoleCommand(text) {
+ OV.log(`Hub ConsoleCommand: ${text}`);
+ return command.run(null, text);
+ },
+
+ Think() {
+ timer.tick();
+ }
+ };
+
+ gamemode.set(GM);
+})();
+JS
+
+echo "[openvibe] writing scoped prototype gamemodes"
+cat > game/openvibe.games/js/gamemodes/prophunt/server.js <<'JS'
+(function () {
+ const props = ["crate", "barrel", "chair", "bucket"];
+
+ function randomProp() {
+ return props[Math.floor(Math.random() * props.length)];
+ }
+
+ command.add("prop", "Prop Hunt disguise command.", function (ply, args) {
+ if (!ply) {
+ OV.log("prop command requires player");
+ return false;
+ }
+
+ const prop = args[0] || randomProp();
+ ply.runCommand(`ov_prophunt_disguise ${prop}`);
+ return false;
+ });
+
+ const GM = {
+ mode: "prophunt",
+ name: "OpenVibe Prop Hunt",
+
+ Initialize() {
+ OV.log("Prop Hunt Initialize fired");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Prop Hunt: hide as props or hunt them down.");
+ },
+
+ PlayerSay(ply, text) {
+ const msg = String(text || "").trim();
+ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));
+ return undefined;
+ },
+
+ Think() {
+ timer.tick();
+ }
+ };
+
+ gamemode.set(GM);
+})();
+JS
+
+cat > game/openvibe.games/js/gamemodes/deathrun/server.js <<'JS'
+(function () {
+ command.add("finish", "Deathrun finish smoke command.", function (ply) {
+ if (ply) ply.chat("Deathrun finish test.");
+ return false;
+ });
+
+ const GM = {
+ mode: "deathrun",
+ name: "OpenVibe Deathrun",
+
+ Initialize() {
+ OV.log("Deathrun Initialize fired");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Deathrun: survive the traps and reach the finish.");
+ },
+
+ PlayerSay(ply, text) {
+ const msg = String(text || "").trim();
+ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));
+ return undefined;
+ },
+
+ Think() {
+ timer.tick();
+ }
+ };
+
+ gamemode.set(GM);
+})();
+JS
+
+cat > game/openvibe.games/js/gamemodes/fortwars/server.js <<'JS'
+(function () {
+ const allowed = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);
+
+ command.add("build", "Fort Wars build command.", function (ply, args) {
+ if (!ply) {
+ OV.log("build command requires player");
+ return false;
+ }
+
+ const part = args[0] || "crate";
+ if (!allowed.has(part)) {
+ ply.chat("Allowed: crate, barrel, pallet, fence, sheet");
+ return false;
+ }
+
+ ply.runCommand(`ov_fortwars_spawn ${part}`);
+ return false;
+ });
+
+ const GM = {
+ mode: "fortwars",
+ name: "OpenVibe Fort Wars",
+
+ Initialize() {
+ OV.log("Fort Wars Initialize fired");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Fort Wars: build first, fight second.");
+ },
+
+ PlayerSay(ply, text) {
+ const msg = String(text || "").trim();
+ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));
+ return undefined;
+ },
+
+ Think() {
+ timer.tick();
+ }
+ };
+
+ gamemode.set(GM);
+})();
+JS
+
+cat > game/openvibe.games/js/gamemodes/traitortown/server.js <<'JS'
+(function () {
+ const roles = new Map();
+
+ command.add("role", "Show your Traitor Town role.", function (ply) {
+ if (!ply) {
+ OV.log("role command requires player");
+ return false;
+ }
+
+ ply.chat(`Your role: ${roles.get(ply.userId()) || "innocent"}`);
+ return false;
+ });
+
+ const GM = {
+ mode: "traitortown",
+ name: "OpenVibe Traitor Town",
+
+ Initialize() {
+ OV.log("Traitor Town Initialize fired");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Traitor Town: find the traitors before they find you.");
+ roles.set(ply.userId(), "innocent");
+ },
+
+ PlayerSay(ply, text) {
+ const msg = String(text || "").trim();
+ if (msg.startsWith("!")) return command.run(ply, msg.slice(1));
+ return undefined;
+ },
+
+ PlayerDeath(victim, _attacker) {
+ OV.broadcast(`${victim.name()} died.`);
+ },
+
+ Think() {
+ timer.tick();
+ }
+ };
+
+ gamemode.set(GM);
+})();
+JS
+
+echo "[openvibe] patching runtime core load order"
+backup_file sdk/openvibe/shared/ov_js_runtime.cpp
+python3 <<'PY'
+from pathlib import Path
+import re
+
+p = Path("sdk/openvibe/shared/ov_js_runtime.cpp")
+s = p.read_text()
+
+new_func = r'''bool COpenVibeJSRuntime::LoadCoreFiles()
+{
+ if (!LoadFile("js/core/hook.js"))
+ return false;
+
+ if (!LoadFile("js/core/gamemode.js"))
+ return false;
+
+ if (!LoadFile("js/bridge.js"))
+ return false;
+
+ if (!LoadFile("js/core/command.js"))
+ return false;
+
+ if (!LoadFile("js/core/timer.js"))
+ return false;
+
+ return true;
+}'''
+
+s2 = re.sub(
+ r'bool\s+COpenVibeJSRuntime::LoadCoreFiles\s*\(\s*\)\s*\{.*?\n\}',
+ new_func,
+ s,
+ count=1,
+ flags=re.S,
+)
+
+if s2 == s:
+ raise SystemExit("Could not patch COpenVibeJSRuntime::LoadCoreFiles")
+
+p.write_text(s2)
+PY
+
+echo "[openvibe] writing robust Node JS core smoke test"
+backup_file tools/smoke-js-core-node.mjs
+cat > tools/smoke-js-core-node.mjs <<'MJS'
+import fs from "node:fs";
+import path from "node:path";
+import vm from "node:vm";
+
+const root = process.env.OPENVIBE_ROOT || process.cwd();
+let fakeTime = 0;
+const logs = [];
+const broadcasts = [];
+const chats = [];
+const serverCommands = [];
+
+const context = {
+ console,
+ globalThis: null,
+ OV: {
+ log: (msg) => logs.push(String(msg)),
+ warn: (msg) => logs.push(`WARN ${String(msg)}`),
+ error: (msg) => logs.push(`ERROR ${String(msg)}`),
+ getMode: () => "hub",
+ getMapName: () => "ov_hub",
+ time: () => fakeTime,
+ broadcast: (msg) => broadcasts.push(String(msg)),
+ players: () => [mockPlayer],
+ playerByUserId: (id) => Number(id) === 1 ? mockPlayer : null,
+ serverCommand: (cmd) => serverCommands.push(String(cmd)),
+ fireHook: (...args) => context.gamemode.call(...args),
+ reward: () => undefined,
+ endMatch: () => undefined
+ }
+};
+
+const mockPlayer = {
+ userId: () => 1,
+ entIndex: () => 1,
+ steamId: () => "STEAM_0:1:1",
+ name: () => "SmokePlayer",
+ health: () => 100,
+ setHealth: () => undefined,
+ team: () => 0,
+ setTeam: () => undefined,
+ chat: (msg) => chats.push(String(msg)),
+ runCommand: (cmd) => serverCommands.push(String(cmd))
+};
+
+context.globalThis = context;
+vm.createContext(context);
+
+function load(relativePath) {
+ const full = path.join(root, relativePath);
+ const source = fs.readFileSync(full, "utf8");
+ vm.runInContext(source, context, { filename: relativePath });
+}
+
+for (const file of [
+ "game/openvibe.games/js/core/hook.js",
+ "game/openvibe.games/js/core/gamemode.js",
+ "game/openvibe.games/js/bridge.js",
+ "game/openvibe.games/js/core/command.js",
+ "game/openvibe.games/js/core/timer.js",
+ "game/openvibe.games/js/gamemodes/base/server.js",
+ "game/openvibe.games/js/gamemodes/hub/server.js"
+]) {
+ load(file);
+}
+
+if (!context.hook) throw new Error("hook global missing");
+if (!context.gamemode) throw new Error("gamemode global missing");
+if (!context.command) throw new Error("command global missing");
+if (!context.timer) throw new Error("timer global missing");
+if (!context.gamemode.get()) throw new Error("gamemode was not set");
+
+context.gamemode.call("Initialize");
+context.gamemode.call("MapInitialize", "ov_hub");
+context.gamemode.call("ConsoleCommand", "help");
+context.gamemode.call("ConsoleCommand", "smoke");
+context.gamemode.call("ConsoleCommand", "timer_smoke");
+context.gamemode.call("ConsoleCommand", "hub_status");
+context.gamemode.call("PlayerInitialSpawn", mockPlayer);
+context.gamemode.call("PlayerSay", mockPlayer, "!help");
+context.gamemode.call("PlayerSay", mockPlayer, "!where");
+context.gamemode.call("PlayerSay", mockPlayer, "!js");
+context.gamemode.call("PlayerSay", mockPlayer, "!hp");
+context.gamemode.call("PlayerSay", mockPlayer, "!players");
+fakeTime += 1;
+context.gamemode.call("Think");
+context.timer.tick();
+
+const combined = [...logs, ...broadcasts, ...chats, ...serverCommands].join("\n");
+
+for (const expected of [
+ "Hub Initialize fired",
+ "OpenVibe embedded JS smoke test passed.",
+ "OpenVibe timer smoke fired.",
+ "JavaScript hooks are working.",
+ "Health: 100",
+ "Players online: 1",
+ "You are on ov_hub in hub mode."
+]) {
+ if (!combined.includes(expected)) {
+ console.error(combined);
+ throw new Error(`missing expected smoke output: ${expected}`);
+ }
+}
+
+console.log("[openvibe-smoke] JS core command/timer smoke passed");
+console.log(`[openvibe-smoke] logs=${logs.length} broadcasts=${broadcasts.length} chats=${chats.length}`);
+MJS
+
+echo "[openvibe] run JS core smoke test"
+node tools/smoke-js-core-node.mjs
+
+echo "[openvibe] apply SDK patch"
+tools/apply-openvibe-sdk.sh
+
+echo "[openvibe] build SDK"
+tools/build-sdk-linux.sh 2>&1 | tee "$HOME/ov-build.log"
+
+echo "[openvibe] setup OpenVibe bin"
+tools/setup-openvibe-bin.sh
+
+echo
+printf '%s\n' "[openvibe] JS command/timer phase complete."
+printf '%s\n' ""
+printf '%s\n' "Runtime test:"
+printf '%s\n' " OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh"
+printf '%s\n' ""
+printf '%s\n' "Server console tests:"
+printf '%s\n' " ov_js_status"
+printf '%s\n' " ov_js_fire Initialize"
+printf '%s\n' " ov_js_cmd help"
+printf '%s\n' " ov_js_cmd smoke"
+printf '%s\n' " ov_js_cmd timer_smoke"
+printf '%s\n' " ov_js_cmd hub_status"
+printf '%s\n' ""
+printf '%s\n' "Client tests:"
+printf '%s\n' " connect 127.0.0.1:27015"
+printf '%s\n' " say !help"
+printf '%s\n' " say !js"
+printf '%s\n' " say !hp"
+printf '%s\n' " say !players"
+printf '%s\n' " say !where"
new file mode 100755
new file mode 100755
@@ -0,0 +1,676 @@
@@ -0,0 +1,676 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+SDK="${OPENVIBE_SDK:-$ROOT/engine/source-sdk-2013}"
+STAMP="$(date +%Y%m%d-%H%M%S)"
+
+cd "$ROOT"
+
+echo "[openvibe] fix LoadCoreFiles patch + finish JS command/timer phase"
+echo "[openvibe] root=$ROOT"
+echo "[openvibe] sdk=$SDK"
+
+backup_file() {
+ local f="$1"
+ [[ -f "$f" ]] && cp "$f" "$f.bak.$STAMP"
+}
+
+mkdir -p game/openvibe.games/js/core
+mkdir -p game/openvibe.games/js/gamemodes/base
+mkdir -p game/openvibe.games/js/gamemodes/hub
+mkdir -p game/openvibe.games/js/gamemodes/prophunt
+mkdir -p game/openvibe.games/js/gamemodes/deathrun
+mkdir -p game/openvibe.games/js/gamemodes/fortwars
+mkdir -p game/openvibe.games/js/gamemodes/traitortown
+mkdir -p tools
+
+echo "[openvibe] writing canonical JS core files"
+
+cat > game/openvibe.games/js/core/hook.js <<'JS'
+(function () {
+ const buckets = Object.create(null);
+
+ function ensure(name) {
+ name = String(name || "");
+ if (!buckets[name]) buckets[name] = [];
+ return buckets[name];
+ }
+
+ globalThis.hook = {
+ add(name, id, fn) {
+ if (!name || !id || typeof fn !== "function") {
+ throw new Error("hook.add(name, id, fn) requires a hook name, id, and function");
+ }
+
+ const list = ensure(name);
+ const sid = String(id);
+ for (let i = list.length - 1; i >= 0; --i) {
+ if (list[i].id === sid) list.splice(i, 1);
+ }
+ list.push({ id: sid, fn });
+ },
+
+ remove(name, id) {
+ const list = buckets[String(name || "")];
+ if (!list) return;
+ const sid = String(id || "");
+ for (let i = list.length - 1; i >= 0; --i) {
+ if (list[i].id === sid) list.splice(i, 1);
+ }
+ },
+
+ run(name, ...args) {
+ const list = buckets[String(name || "")];
+ if (!list) return undefined;
+
+ const snapshot = list.slice();
+ for (const item of snapshot) {
+ const result = item.fn(...args);
+ if (result !== undefined) return result;
+ }
+
+ return undefined;
+ },
+
+ list(name) {
+ if (name) return (buckets[String(name)] || []).map((item) => item.id);
+ const out = Object.create(null);
+ for (const key of Object.keys(buckets)) out[key] = buckets[key].map((item) => item.id);
+ return out;
+ }
+ };
+})();
+JS
+
+cat > game/openvibe.games/js/core/gamemode.js <<'JS'
+(function () {
+ let current = null;
+
+ globalThis.gamemode = {
+ set(gm) {
+ if (!gm || typeof gm !== "object") throw new Error("gamemode.set(gm) requires an object");
+ current = gm;
+ return gm;
+ },
+
+ get() {
+ return current;
+ },
+
+ call(name, ...args) {
+ const hookResult = hook.run(name, ...args);
+ if (hookResult !== undefined) return hookResult;
+
+ if (current && typeof current[name] === "function") {
+ return current[name](...args);
+ }
+
+ return undefined;
+ }
+ };
+})();
+JS
+
+cat > game/openvibe.games/js/bridge.js <<'JS'
+(function () {
+ if (globalThis.OV && typeof OV.log === "function") {
+ OV.log("bridge.js loaded");
+ }
+})();
+JS
+
+cat > game/openvibe.games/js/core/command.js <<'JS'
+(function () {
+ const commands = Object.create(null);
+
+ function normalize(name) {
+ return String(name || "").trim().replace(/^!+/, "").toLowerCase();
+ }
+
+ function split(text) {
+ const raw = String(text || "").trim();
+ if (!raw) return { name: "", args: [], raw: "" };
+ const parts = raw.split(/\s+/g);
+ const name = normalize(parts.shift());
+ return { name, args: parts, raw };
+ }
+
+ function reply(ply, msg) {
+ if (ply && typeof ply.chat === "function") ply.chat(String(msg));
+ else if (globalThis.OV && typeof OV.broadcast === "function") OV.broadcast(String(msg));
+ else if (globalThis.OV && typeof OV.log === "function") OV.log(String(msg));
+ }
+
+ function listNames() {
+ return Object.keys(commands).sort();
+ }
+
+ globalThis.command = {
+ add(name, helpOrFn, maybeFn) {
+ const key = normalize(name);
+ if (!key) throw new Error("command.add requires a command name");
+
+ let help = "";
+ let fn = helpOrFn;
+ if (typeof helpOrFn === "string") {
+ help = helpOrFn;
+ fn = maybeFn;
+ }
+
+ if (typeof fn !== "function") throw new Error("command.add requires a function");
+ commands[key] = { help, fn };
+ },
+
+ remove(name) {
+ delete commands[normalize(name)];
+ },
+
+ has(name) {
+ return !!commands[normalize(name)];
+ },
+
+ list: listNames,
+
+ run(ply, text) {
+ const parsed = split(text);
+ if (!parsed.name) return undefined;
+
+ const entry = commands[parsed.name];
+ if (!entry) {
+ reply(ply, `Unknown command: ${parsed.name}. Try !help`);
+ return false;
+ }
+
+ const result = entry.fn({ ply, name: parsed.name, args: parsed.args, raw: parsed.raw, reply });
+ return result === undefined ? false : result;
+ },
+
+ dispatchConsole(text) {
+ return this.run(null, text);
+ }
+ };
+
+ hook.add("PlayerSay", "OpenVibeCommandRegistry", function (ply, text) {
+ const msg = String(text || "").trim();
+ if (!msg.startsWith("!")) return undefined;
+ return command.run(ply, msg.slice(1));
+ });
+
+ hook.add("ConsoleCommand", "OpenVibeConsoleCommandRegistry", function (text) {
+ return command.dispatchConsole(text);
+ });
+
+ command.add("help", "List OpenVibe JS commands", function ({ reply, ply }) {
+ reply(ply, `Commands: ${listNames().map((name) => `!${name}`).join(", ")}`);
+ return false;
+ });
+
+ command.add("smoke", "Run the embedded JS smoke test", function ({ reply, ply }) {
+ reply(ply, "OpenVibe embedded JS smoke test passed.");
+ return false;
+ });
+
+ command.add("timer_smoke", "Run the JS timer smoke test", function ({ reply, ply }) {
+ reply(ply, "Timer smoke scheduled.");
+ if (globalThis.timer && typeof timer.simple === "function") {
+ timer.simple(0.05, function () {
+ if (globalThis.OV && typeof OV.broadcast === "function") {
+ OV.broadcast("OpenVibe JS timer smoke fired.");
+ }
+ });
+ }
+ return false;
+ });
+})();
+JS
+
+cat > game/openvibe.games/js/core/timer.js <<'JS'
+(function () {
+ const timers = Object.create(null);
+ let nextAutoId = 1;
+
+ function now() {
+ if (globalThis.OV && typeof OV.time === "function") return Number(OV.time()) || 0;
+ return Date.now() / 1000;
+ }
+
+ function create(id, delay, reps, fn) {
+ if (!id) id = `timer_${nextAutoId++}`;
+ if (typeof fn !== "function") throw new Error("timer.create requires a callback");
+
+ delay = Math.max(0, Number(delay) || 0);
+ reps = Number(reps);
+ if (!Number.isFinite(reps)) reps = 1;
+
+ timers[String(id)] = {
+ id: String(id),
+ delay,
+ reps,
+ count: 0,
+ next: now() + delay,
+ fn
+ };
+
+ return String(id);
+ }
+
+ function remove(id) {
+ delete timers[String(id || "")];
+ }
+
+ function tick() {
+ const t = now();
+ for (const id of Object.keys(timers)) {
+ const item = timers[id];
+ if (!item || t < item.next) continue;
+
+ item.count += 1;
+ try {
+ item.fn(item.count, id);
+ } catch (err) {
+ if (globalThis.OV && typeof OV.error === "function") OV.error(`timer ${id}: ${err && err.message ? err.message : err}`);
+ delete timers[id];
+ continue;
+ }
+
+ if (item.reps > 0 && item.count >= item.reps) {
+ delete timers[id];
+ } else {
+ item.next = t + item.delay;
+ }
+ }
+ }
+
+ globalThis.timer = {
+ create,
+ remove,
+ exists(id) {
+ return !!timers[String(id || "")];
+ },
+ simple(delay, fn) {
+ return create(`simple_${nextAutoId++}`, delay, 1, fn);
+ },
+ tick,
+ list() {
+ return Object.keys(timers).sort();
+ }
+ };
+
+ hook.add("Think", "OpenVibeTimerThink", tick);
+})();
+JS
+
+echo "[openvibe] writing scoped gamemode scripts"
+
+cat > game/openvibe.games/js/gamemodes/base/server.js <<'JS'
+(function () {
+ const BaseServerGM = {
+ mode: "base",
+ name: "OpenVibe Base",
+
+ Initialize() {
+ OV.log("Base Initialize fired");
+ },
+
+ MapInitialize(mapName) {
+ OV.log(`Base MapInitialize: ${mapName}`);
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source.");
+ },
+
+ PlayerSpawn(_ply) {},
+ PlayerDeath(_victim, _attacker) {},
+ PlayerDisconnected(_ply) {},
+ Think() {}
+ };
+
+ gamemode.set(BaseServerGM);
+})();
+JS
+
+cat > game/openvibe.games/js/gamemodes/hub/server.js <<'JS'
+(function () {
+ function registerHubCommands() {
+ if (!globalThis.command) return;
+
+ command.add("js", "Confirm JavaScript hooks are working", function ({ ply, reply }) {
+ reply(ply, "JavaScript hooks are working.");
+ return false;
+ });
+
+ command.add("hp", "Show current health", function ({ ply, reply }) {
+ if (!ply) return false;
+ reply(ply, `Health: ${ply.health()}`);
+ return false;
+ });
+
+ command.add("players", "Show player count", function ({ ply, reply }) {
+ reply(ply, `Players online: ${OV.players().length}`);
+ return false;
+ });
+
+ command.add("where", "Show current mode and map", function ({ ply, reply }) {
+ reply(ply, `Mode=${OV.getMode()} map=${OV.getMapName()}`);
+ return false;
+ });
+
+ command.add("hub_status", "Broadcast hub status", function ({ ply, reply }) {
+ reply(ply, `Hub OK. mode=${OV.getMode()} map=${OV.getMapName()} players=${OV.players().length}`);
+ return false;
+ });
+ }
+
+ const HubServerGM = {
+ mode: "hub",
+ name: "OpenVibe Hub",
+
+ Initialize() {
+ OV.log("Hub Initialize fired");
+ registerHubCommands();
+ },
+
+ MapInitialize(mapName) {
+ OV.log(`Hub MapInitialize: ${mapName}`);
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source JS runtime.");
+ OV.broadcast(`${ply.name()} joined the hub.`);
+ },
+
+ PlayerSpawn(ply) {
+ ply.chat("PlayerSpawn hook fired.");
+ },
+
+ PlayerSay(_ply, _text) {
+ return undefined;
+ },
+
+ ConsoleCommand(text) {
+ OV.log(`Hub ConsoleCommand fallback: ${text}`);
+ return undefined;
+ },
+
+ Think() {}
+ };
+
+ gamemode.set(HubServerGM);
+})();
+JS
+
+cat > game/openvibe.games/js/gamemodes/prophunt/server.js <<'JS'
+(function () {
+ function registerCommands() {
+ if (!globalThis.command) return;
+ command.add("ph_status", "Show Prop Hunt status", function ({ ply, reply }) {
+ reply(ply, `Prop Hunt JS online. map=${OV.getMapName()} players=${OV.players().length}`);
+ return false;
+ });
+ command.add("disguise", "Disguise as an allowlisted prop", function ({ args, ply, reply }) {
+ const choice = args[0] || "crate";
+ if (!ply) return false;
+ ply.runCommand(`ov_prophunt_disguise ${choice}`);
+ reply(ply, `Trying prop disguise: ${choice}`);
+ return false;
+ });
+ }
+
+ const PropHuntGM = {
+ mode: "prophunt",
+ name: "OpenVibe Prop Hunt",
+ Initialize() { OV.log("Prop Hunt Initialize fired"); registerCommands(); },
+ PlayerInitialSpawn(ply) { ply.chat("Prop Hunt JS loaded. Try !ph_status or !disguise crate"); },
+ Think() {}
+ };
+ gamemode.set(PropHuntGM);
+})();
+JS
+
+cat > game/openvibe.games/js/gamemodes/deathrun/server.js <<'JS'
+(function () {
+ function registerCommands() {
+ if (!globalThis.command) return;
+ command.add("dr_status", "Show Deathrun status", function ({ ply, reply }) {
+ reply(ply, `Deathrun JS online. map=${OV.getMapName()} players=${OV.players().length}`);
+ return false;
+ });
+ }
+
+ const DeathrunGM = {
+ mode: "deathrun",
+ name: "OpenVibe Deathrun",
+ Initialize() { OV.log("Deathrun Initialize fired"); registerCommands(); },
+ PlayerInitialSpawn(ply) { ply.chat("Deathrun JS loaded. Try !dr_status"); },
+ Think() {}
+ };
+ gamemode.set(DeathrunGM);
+})();
+JS
+
+cat > game/openvibe.games/js/gamemodes/fortwars/server.js <<'JS'
+(function () {
+ function registerCommands() {
+ if (!globalThis.command) return;
+ command.add("fw_status", "Show Fort Wars status", function ({ ply, reply }) {
+ reply(ply, `Fort Wars JS online. map=${OV.getMapName()} players=${OV.players().length}`);
+ return false;
+ });
+ command.add("build", "Spawn an allowlisted Fort Wars prop", function ({ args, ply, reply }) {
+ const choice = args[0] || "crate";
+ if (!ply) return false;
+ ply.runCommand(`ov_fortwars_spawn ${choice}`);
+ reply(ply, `Trying Fort Wars prop: ${choice}`);
+ return false;
+ });
+ }
+
+ const FortWarsGM = {
+ mode: "fortwars",
+ name: "OpenVibe Fort Wars",
+ Initialize() { OV.log("Fort Wars Initialize fired"); registerCommands(); },
+ PlayerInitialSpawn(ply) { ply.chat("Fort Wars JS loaded. Try !fw_status or !build crate"); },
+ Think() {}
+ };
+ gamemode.set(FortWarsGM);
+})();
+JS
+
+cat > game/openvibe.games/js/gamemodes/traitortown/server.js <<'JS'
+(function () {
+ function registerCommands() {
+ if (!globalThis.command) return;
+ command.add("ttt_status", "Show Traitor Town status", function ({ ply, reply }) {
+ reply(ply, `Traitor Town JS online. map=${OV.getMapName()} players=${OV.players().length}`);
+ return false;
+ });
+ }
+
+ const TraitorTownGM = {
+ mode: "traitortown",
+ name: "OpenVibe Traitor Town",
+ Initialize() { OV.log("Traitor Town Initialize fired"); registerCommands(); },
+ PlayerInitialSpawn(ply) { ply.chat("Traitor Town JS loaded. Try !ttt_status"); },
+ Think() {}
+ };
+ gamemode.set(TraitorTownGM);
+})();
+JS
+
+echo "[openvibe] robustly replacing COpenVibeJSRuntime::LoadCoreFiles"
+
+backup_file sdk/openvibe/shared/ov_js_runtime.cpp
+
+python3 <<'PY'
+from pathlib import Path
+
+p = Path("sdk/openvibe/shared/ov_js_runtime.cpp")
+s = p.read_text()
+
+sig = "bool COpenVibeJSRuntime::LoadCoreFiles()"
+idx = s.find(sig)
+if idx < 0:
+ raise SystemExit("Could not find COpenVibeJSRuntime::LoadCoreFiles signature")
+
+brace = s.find("{", idx)
+if brace < 0:
+ raise SystemExit("Could not find COpenVibeJSRuntime::LoadCoreFiles opening brace")
+
+depth = 0
+end = None
+for i in range(brace, len(s)):
+ ch = s[i]
+ if ch == "{":
+ depth += 1
+ elif ch == "}":
+ depth -= 1
+ if depth == 0:
+ end = i + 1
+ break
+
+if end is None:
+ raise SystemExit("Could not find COpenVibeJSRuntime::LoadCoreFiles closing brace")
+
+replacement = '''bool COpenVibeJSRuntime::LoadCoreFiles()
+{
+ if (!LoadFile("js/core/hook.js"))
+ return false;
+
+ if (!LoadFile("js/core/gamemode.js"))
+ return false;
+
+ if (!LoadFile("js/bridge.js"))
+ return false;
+
+ if (!LoadFile("js/core/command.js"))
+ return false;
+
+ if (!LoadFile("js/core/timer.js"))
+ return false;
+
+ return true;
+}'''
+
+s = s[:idx] + replacement + s[end:]
+p.write_text(s)
+print("[openvibe] LoadCoreFiles replaced")
+PY
+
+echo "[openvibe] writing Node JS smoke test"
+
+cat > tools/smoke-js-core-node.mjs <<'JS'
+import fs from "node:fs";
+import vm from "node:vm";
+
+let fakeTime = 0;
+const messages = [];
+
+function player() {
+ return {
+ userId: 1,
+ entIndex: 1,
+ steamId: () => "STEAM_0:1:1",
+ name: () => "SmokePlayer",
+ health: () => 87,
+ setHealth: () => undefined,
+ chat: (msg) => messages.push(`[chat] ${msg}`),
+ team: () => 0,
+ setTeam: () => undefined,
+ runCommand: (cmd) => messages.push(`[runCommand] ${cmd}`)
+ };
+}
+
+const context = vm.createContext({
+ console,
+ globalThis: {},
+ OV: {
+ log: (msg) => messages.push(`[log] ${msg}`),
+ warn: (msg) => messages.push(`[warn] ${msg}`),
+ error: (msg) => messages.push(`[error] ${msg}`),
+ getMode: () => "hub",
+ getMapName: () => "ov_hub",
+ time: () => fakeTime,
+ broadcast: (msg) => messages.push(`[broadcast] ${msg}`),
+ players: () => [player()],
+ playerByUserId: () => player(),
+ serverCommand: (cmd) => messages.push(`[serverCommand] ${cmd}`),
+ reward: () => undefined,
+ endMatch: () => undefined
+ }
+});
+context.globalThis = context;
+
+const files = [
+ "game/openvibe.games/js/core/hook.js",
+ "game/openvibe.games/js/core/gamemode.js",
+ "game/openvibe.games/js/bridge.js",
+ "game/openvibe.games/js/core/command.js",
+ "game/openvibe.games/js/core/timer.js",
+ "game/openvibe.games/js/gamemodes/base/server.js",
+ "game/openvibe.games/js/gamemodes/hub/server.js"
+];
+
+for (const file of files) {
+ const code = fs.readFileSync(file, "utf8");
+ vm.runInContext(code, context, { filename: file });
+}
+
+context.gamemode.call("Initialize");
+context.gamemode.call("MapInitialize", "ov_hub");
+context.gamemode.call("PlayerInitialSpawn", player());
+
+const blocked = context.gamemode.call("PlayerSay", player(), "!js");
+if (blocked !== false) throw new Error("!js did not block default chat");
+
+context.gamemode.call("ConsoleCommand", "smoke");
+context.gamemode.call("ConsoleCommand", "hub_status");
+context.gamemode.call("ConsoleCommand", "timer_smoke");
+fakeTime += 1;
+context.gamemode.call("Think");
+
+if (!messages.some((line) => line.includes("JavaScript hooks are working"))) throw new Error("missing !js response");
+if (!messages.some((line) => line.includes("OpenVibe embedded JS smoke test passed"))) throw new Error("missing smoke response");
+if (!messages.some((line) => line.includes("OpenVibe JS timer smoke fired"))) throw new Error("missing timer smoke response");
+
+console.log(messages.join("\n"));
+console.log("[openvibe-smoke] JS command/timer smoke passed");
+JS
+
+echo "[openvibe] run JS smoke test"
+node tools/smoke-js-core-node.mjs
+
+echo "[openvibe] apply SDK patch"
+tools/apply-openvibe-sdk.sh
+
+echo "[openvibe] build SDK"
+if [[ "${RUN_BUILD:-1}" = "1" ]]; then
+ tools/build-sdk-linux.sh 2>&1 | tee "$HOME/ov-build.log"
+fi
+
+echo "[openvibe] setup OpenVibe bin"
+tools/setup-openvibe-bin.sh
+
+echo
+
+echo "[openvibe] JS command/timer phase complete."
+echo
+
+echo "Runtime test:"
+echo " OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh"
+echo
+echo "Server console tests:"
+echo " ov_js_status"
+echo " ov_js_cmd help"
+echo " ov_js_cmd smoke"
+echo " ov_js_cmd timer_smoke"
+echo " ov_js_cmd hub_status"
+echo
+echo "Client tests:"
+echo " connect 127.0.0.1:27015"
+echo " say !help"
+echo " say !js"
+echo " say !hp"
+echo " say !players"
+echo " say !where"
new file mode 100755
new file mode 100755
@@ -0,0 +1,665 @@
@@ -0,0 +1,665 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="${OPENVIBE_ROOT:-$HOME/src/openvibe-source}"
+SDK="${OPENVIBE_SDK:-$ROOT/engine/source-sdk-2013}"
+STAMP="$(date +%Y%m%d-%H%M%S)"
+RUN_BUILD="${RUN_BUILD:-1}"
+
+cd "$ROOT"
+
+echo "[openvibe] next phase: JS command registry + timers + smoke tests"
+echo "[openvibe] root=$ROOT"
+echo "[openvibe] sdk=$SDK"
+
+backup_file() {
+ local file="$1"
+ [[ -f "$file" ]] && cp "$file" "$file.bak.$STAMP"
+}
+
+need_file() {
+ [[ -f "$1" ]] || { echo "[openvibe] missing required file: $1" >&2; exit 1; }
+}
+
+need_file sdk/openvibe/shared/ov_js_runtime.cpp
+need_file tools/apply-openvibe-sdk.sh
+need_file tools/build-sdk-linux.sh
+need_file tools/setup-openvibe-bin.sh
+
+mkdir -p game/openvibe.games/js/core game/openvibe.games/js/gamemodes/{base,hub,prophunt,deathrun,fortwars,traitortown} tools
+
+echo "[openvibe] writing JS core command registry"
+
+cat > game/openvibe.games/js/core/command.js <<'JS_COMMAND'
+(function () {
+ const chatCommands = new Map();
+ const consoleCommands = new Map();
+
+ function splitWords(text) {
+ const source = String(text || "").trim();
+ if (!source) return [];
+ return source.split(/\s+/g);
+ }
+
+ function normalizeChatName(name) {
+ return String(name || "").replace(/^!+/, "").toLowerCase();
+ }
+
+ function normalizeConsoleName(name) {
+ return String(name || "").toLowerCase();
+ }
+
+ function register(map, name, fn, help) {
+ if (typeof fn !== "function") throw new Error("command callback must be function");
+ const key = map === chatCommands ? normalizeChatName(name) : normalizeConsoleName(name);
+ if (!key) throw new Error("command name is required");
+ map.set(key, { fn, help: help ? String(help) : "" });
+ }
+
+ function runChat(ply, text) {
+ const words = splitWords(text);
+ if (words.length === 0) return undefined;
+ if (words[0][0] !== "!") return undefined;
+
+ const name = normalizeChatName(words[0]);
+ const entry = chatCommands.get(name);
+ if (!entry) return undefined;
+
+ try {
+ const args = words.slice(1);
+ const result = entry.fn(ply, args, String(text || ""));
+ return result === undefined ? false : result;
+ } catch (err) {
+ OV.error(`[chat:${name}] ${err && err.stack ? err.stack : err}`);
+ if (ply && typeof ply.chat === "function") ply.chat("Command failed. Check server console.");
+ return false;
+ }
+ }
+
+ function runConsole(text) {
+ const words = splitWords(text);
+ if (words.length === 0) return undefined;
+
+ const name = normalizeConsoleName(words[0]);
+ const entry = consoleCommands.get(name);
+ if (!entry) return undefined;
+
+ try {
+ const args = words.slice(1);
+ const result = entry.fn(args, String(text || ""));
+ return result === undefined ? false : result;
+ } catch (err) {
+ OV.error(`[console:${name}] ${err && err.stack ? err.stack : err}`);
+ return false;
+ }
+ }
+
+ function list(map) {
+ const out = [];
+ for (const [name, entry] of map.entries()) out.push({ name, help: entry.help });
+ out.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
+ return out;
+ }
+
+ globalThis.command = {
+ chat(name, fn, help) { register(chatCommands, name, fn, help); },
+ console(name, fn, help) { register(consoleCommands, name, fn, help); },
+ removeChat(name) { return chatCommands.delete(normalizeChatName(name)); },
+ removeConsole(name) { return consoleCommands.delete(normalizeConsoleName(name)); },
+ runChat,
+ runConsole,
+ chatList() { return list(chatCommands); },
+ consoleList() { return list(consoleCommands); }
+ };
+
+ globalThis.concommand = {
+ add(name, fn, help) { register(consoleCommands, name, fn, help); },
+ remove(name) { return consoleCommands.delete(normalizeConsoleName(name)); },
+ list() { return list(consoleCommands); }
+ };
+
+ hook.add("PlayerSay", "openvibe.command.chat", runChat);
+ hook.add("ConsoleCommand", "openvibe.command.console", runConsole);
+
+ OV.log("command.js loaded");
+})();
+JS_COMMAND
+
+echo "[openvibe] writing JS timer library"
+
+cat > game/openvibe.games/js/core/timer.js <<'JS_TIMER'
+(function () {
+ const timers = new Map();
+ let nextId = 1;
+
+ function now() {
+ return Number(OV.time() || 0);
+ }
+
+ function create(id, delay, reps, fn) {
+ if (typeof fn !== "function") throw new Error("timer callback must be function");
+
+ const key = String(id || `timer_${nextId++}`);
+ const seconds = Math.max(0, Number(delay || 0));
+ const repeatCount = reps === undefined ? 1 : Number(reps);
+
+ timers.set(key, {
+ id: key,
+ delay: seconds,
+ reps: repeatCount,
+ runs: 0,
+ next: now() + seconds,
+ fn
+ });
+
+ return key;
+ }
+
+ function simple(delay, fn) {
+ return create(null, delay, 1, fn);
+ }
+
+ function remove(id) {
+ return timers.delete(String(id));
+ }
+
+ function exists(id) {
+ return timers.has(String(id));
+ }
+
+ function tick() {
+ const t = now();
+ const due = [];
+
+ for (const item of timers.values()) {
+ if (t >= item.next) due.push(item.id);
+ }
+
+ for (const id of due) {
+ const item = timers.get(id);
+ if (!item) continue;
+
+ try {
+ item.runs += 1;
+ item.fn(item.runs, id);
+ } catch (err) {
+ OV.error(`[timer:${id}] ${err && err.stack ? err.stack : err}`);
+ }
+
+ if (item.reps > 0 && item.runs >= item.reps) {
+ timers.delete(id);
+ } else {
+ item.next = now() + item.delay;
+ }
+ }
+
+ return undefined;
+ }
+
+ function list() {
+ const out = [];
+ const t = now();
+ for (const item of timers.values()) {
+ out.push({
+ id: item.id,
+ delay: item.delay,
+ reps: item.reps,
+ runs: item.runs,
+ remaining: Math.max(0, item.next - t)
+ });
+ }
+ return out;
+ }
+
+ globalThis.timer = { create, simple, remove, exists, list };
+ hook.add("Think", "openvibe.timer.tick", tick);
+
+ OV.log("timer.js loaded");
+})();
+JS_TIMER
+
+echo "[openvibe] patching runtime load order for command.js and timer.js"
+
+backup_file sdk/openvibe/shared/ov_js_runtime.cpp
+python3 <<'PY'
+from pathlib import Path
+
+p = Path("sdk/openvibe/shared/ov_js_runtime.cpp")
+s = p.read_text()
+old = '''bool COpenVibeJSRuntime::LoadCoreFiles()
+{
+ if (!LoadFile("js/core/hook.js"))
+ return false;
+
+ if (!LoadFile("js/core/gamemode.js"))
+ return false;
+
+ if (!LoadFile("js/bridge.js"))
+ return false;
+
+ return true;
+}
+'''
+new = '''bool COpenVibeJSRuntime::LoadCoreFiles()
+{
+ if (!LoadFile("js/core/hook.js"))
+ return false;
+
+ if (!LoadFile("js/core/gamemode.js"))
+ return false;
+
+ if (!LoadFile("js/bridge.js"))
+ return false;
+
+ if (!LoadFile("js/core/command.js"))
+ return false;
+
+ if (!LoadFile("js/core/timer.js"))
+ return false;
+
+ return true;
+}
+'''
+if old not in s:
+ start = s.find("bool COpenVibeJSRuntime::LoadCoreFiles()")
+ if start == -1:
+ raise SystemExit("LoadCoreFiles not found")
+ brace = s.find("{", start)
+ depth = 0
+ end = None
+ for i in range(brace, len(s)):
+ if s[i] == "{":
+ depth += 1
+ elif s[i] == "}":
+ depth -= 1
+ if depth == 0:
+ end = i + 1
+ break
+ if end is None:
+ raise SystemExit("LoadCoreFiles end not found")
+ s = s[:start] + new + s[end:]
+else:
+ s = s.replace(old, new)
+p.write_text(s)
+PY
+
+echo "[openvibe] writing base gamemode with command/timer smoke support"
+
+cat > game/openvibe.games/js/gamemodes/base/server.js <<'JS_BASE'
+const GM = {
+ mode: "base",
+ name: "OpenVibe Base",
+
+ Initialize() {
+ OV.log("Base Initialize fired");
+
+ command.console("help", () => {
+ const chats = command.chatList().map((cmd) => `!${cmd.name}`).join(", ");
+ const consoles = command.consoleList().map((cmd) => cmd.name).join(", ");
+ OV.log(`Chat commands: ${chats || "none"}`);
+ OV.log(`Console commands: ${consoles || "none"}`);
+ return false;
+ }, "List OpenVibe JS commands");
+
+ command.console("smoke", () => {
+ OV.log("OpenVibe embedded JS smoke command passed.");
+ OV.broadcast("OpenVibe embedded JS smoke command passed.");
+ return false;
+ }, "Run embedded JS smoke test");
+
+ command.console("timer_smoke", () => {
+ OV.log("Scheduling timer smoke test.");
+ timer.simple(1.0, () => OV.broadcast("OpenVibe JS timer smoke passed."));
+ return false;
+ }, "Schedule a one-second timer smoke test");
+
+ command.chat("help", (ply) => {
+ const names = command.chatList().map((cmd) => `!${cmd.name}`).join(", ");
+ ply.chat(`Commands: ${names || "none"}`);
+ return false;
+ }, "List chat commands");
+
+ command.chat("js", (ply) => {
+ ply.chat("JavaScript command registry is working.");
+ return false;
+ }, "JS smoke test");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source.");
+ },
+
+ PlayerSpawn(_ply) {},
+
+ PlayerDeath(_victim, _attacker) {},
+
+ PlayerDisconnected(_ply) {},
+
+ PlayerSay(_ply, _text) {
+ return undefined;
+ },
+
+ ConsoleCommand(text) {
+ OV.log(`Unhandled ConsoleCommand: ${text}`);
+ return undefined;
+ },
+
+ Think() {}
+};
+
+gamemode.set(GM);
+JS_BASE
+
+echo "[openvibe] writing hub gamemode using command registry"
+
+cat > game/openvibe.games/js/gamemodes/hub/server.js <<'JS_HUB'
+const GM = {
+ mode: "hub",
+ name: "OpenVibe Hub",
+
+ Initialize() {
+ OV.log("Hub Initialize fired");
+
+ command.chat("hp", (ply) => {
+ ply.chat(`Health: ${ply.health()}`);
+ return false;
+ }, "Show current health");
+
+ command.chat("players", (ply) => {
+ ply.chat(`Players online: ${OV.players().length}`);
+ return false;
+ }, "Show player count");
+
+ command.chat("where", (ply) => {
+ ply.chat(`Mode=${OV.getMode()} Map=${OV.getMapName()}`);
+ return false;
+ }, "Show mode and map");
+
+ command.console("hub_status", () => {
+ OV.log(`Hub status: mode=${OV.getMode()} map=${OV.getMapName()} players=${OV.players().length}`);
+ return false;
+ }, "Print hub status");
+ },
+
+ MapInitialize(mapName) {
+ OV.log(`Map initialized: ${mapName}`);
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Welcome to OpenVibe: Source JS runtime.");
+ ply.chat("Type !help for JS commands.");
+ OV.broadcast(`${ply.name()} joined the hub.`);
+ },
+
+ PlayerSpawn(ply) {
+ timer.simple(0.2, () => ply.chat("PlayerSpawn hook fired."));
+ },
+
+ PlayerSay(_ply, _text) {
+ return undefined;
+ },
+
+ ConsoleCommand(text) {
+ OV.log(`Hub unhandled ConsoleCommand: ${text}`);
+ return undefined;
+ },
+
+ Think() {}
+};
+
+gamemode.set(GM);
+JS_HUB
+
+echo "[openvibe] updating prototype gamemodes to use command registry when available"
+
+cat > game/openvibe.games/js/gamemodes/prophunt/server.js <<'JS_PH'
+const props = ["can", "crate", "barrel", "chair", "bucket"];
+
+function randomProp() {
+ return props[Math.floor(Math.random() * props.length)];
+}
+
+const GM = {
+ mode: "prophunt",
+ name: "OpenVibe Prop Hunt",
+
+ Initialize() {
+ OV.log("Prop Hunt Initialize fired");
+
+ command.chat("prop", (ply, args) => {
+ const prop = args[0] || randomProp();
+ ply.runCommand(`ov_prophunt_disguise ${prop}`);
+ return false;
+ }, "Disguise as a prop");
+
+ command.chat("human", (ply) => {
+ ply.runCommand("ov_prophunt_reset_disguise");
+ return false;
+ }, "Reset disguise");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Prop Hunt: use !prop or !prop crate. Use !human to reset.");
+ },
+
+ Think() {}
+};
+
+gamemode.set(GM);
+JS_PH
+
+cat > game/openvibe.games/js/gamemodes/fortwars/server.js <<'JS_FW'
+const allowed = new Set(["crate", "barrel", "pallet", "fence", "sheet"]);
+
+const GM = {
+ mode: "fortwars",
+ name: "OpenVibe Fort Wars",
+
+ Initialize() {
+ OV.log("Fort Wars Initialize fired");
+
+ command.chat("build", (ply, args) => {
+ const part = args[0] || "";
+ if (!allowed.has(part)) {
+ ply.chat("Allowed: crate, barrel, pallet, fence, sheet");
+ return false;
+ }
+
+ ply.runCommand(`ov_fortwars_spawn ${part}`);
+ return false;
+ }, "Spawn a Fort Wars build prop");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Fort Wars: build first, fight second. Try !build crate.");
+ }
+};
+
+gamemode.set(GM);
+JS_FW
+
+cat > game/openvibe.games/js/gamemodes/deathrun/server.js <<'JS_DR'
+const GM = {
+ mode: "deathrun",
+ name: "OpenVibe Deathrun",
+
+ Initialize() {
+ OV.log("Deathrun Initialize fired");
+
+ command.chat("finish", (ply) => {
+ ply.chat("Deathrun finish test.");
+ OV.reward(ply, 50, 100, "deathrun_finish");
+ return false;
+ }, "Smoke-test Deathrun finish reward path");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Deathrun: survive the traps and reach the finish.");
+ }
+};
+
+gamemode.set(GM);
+JS_DR
+
+cat > game/openvibe.games/js/gamemodes/traitortown/server.js <<'JS_TT'
+const roles = new Map();
+
+const GM = {
+ mode: "traitortown",
+ name: "OpenVibe Traitor Town",
+
+ Initialize() {
+ OV.log("Traitor Town Initialize fired");
+
+ command.chat("role", (ply) => {
+ ply.chat(`Your role: ${roles.get(ply.userId()) || "innocent"}`);
+ return false;
+ }, "Show your role");
+ },
+
+ PlayerInitialSpawn(ply) {
+ ply.chat("Traitor Town: find the traitors before they find you.");
+ roles.set(ply.userId(), "innocent");
+ },
+
+ PlayerDeath(victim, _attacker) {
+ OV.broadcast(`${victim.name()} died.`);
+ }
+};
+
+gamemode.set(GM);
+JS_TT
+
+echo "[openvibe] writing Node-based JS core smoke test"
+
+cat > tools/smoke-js-core-node.mjs <<'NODE_SMOKE'
+import fs from "node:fs";
+import vm from "node:vm";
+
+const files = [
+ "game/openvibe.games/js/core/hook.js",
+ "game/openvibe.games/js/core/gamemode.js",
+ "game/openvibe.games/js/bridge.js",
+ "game/openvibe.games/js/core/command.js",
+ "game/openvibe.games/js/core/timer.js",
+ "game/openvibe.games/js/gamemodes/base/server.js",
+ "game/openvibe.games/js/gamemodes/hub/server.js"
+];
+
+const messages = [];
+let fakeTime = 0;
+
+const context = vm.createContext({
+ console,
+ Map,
+ Set,
+ Math,
+ Number,
+ String,
+ Array,
+ Object,
+ Error,
+ globalThis: {},
+ OV: {
+ log: (msg) => messages.push(`[log] ${msg}`),
+ warn: (msg) => messages.push(`[warn] ${msg}`),
+ error: (msg) => messages.push(`[error] ${msg}`),
+ getMode: () => "hub",
+ getMapName: () => "ov_hub",
+ time: () => fakeTime,
+ broadcast: (msg) => messages.push(`[broadcast] ${msg}`),
+ players: () => [],
+ playerByUserId: () => null,
+ serverCommand: (cmd) => messages.push(`[serverCommand] ${cmd}`),
+ fireHook: () => undefined,
+ reward: () => undefined,
+ endMatch: () => undefined
+ }
+});
+context.globalThis = context;
+
+for (const file of files) {
+ const code = fs.readFileSync(file, "utf8");
+ vm.runInContext(code, context, { filename: file });
+}
+
+context.gamemode.call("Initialize");
+context.gamemode.call("MapInitialize", "ov_hub");
+context.gamemode.call("ConsoleCommand", "smoke");
+context.gamemode.call("ConsoleCommand", "timer_smoke");
+fakeTime = 2;
+context.gamemode.call("Think");
+
+const fakePlayer = {
+ userId: () => 1,
+ entIndex: () => 1,
+ steamId: () => "STEAM_0:1:1",
+ name: () => "SmokePlayer",
+ health: () => 100,
+ setHealth: () => {},
+ chat: (msg) => messages.push(`[chat] ${msg}`),
+ runCommand: (cmd) => messages.push(`[runCommand] ${cmd}`)
+};
+context.gamemode.call("PlayerSay", fakePlayer, "!js");
+context.gamemode.call("PlayerSay", fakePlayer, "!hp");
+context.gamemode.call("PlayerSay", fakePlayer, "!players");
+
+const required = [
+ "command.js loaded",
+ "timer.js loaded",
+ "Hub Initialize fired",
+ "OpenVibe embedded JS smoke command passed.",
+ "OpenVibe JS timer smoke passed.",
+ "JavaScript command registry is working.",
+ "Health: 100",
+ "Players online: 0"
+];
+
+for (const needle of required) {
+ if (!messages.some((line) => line.includes(needle))) {
+ console.error(messages.join("\n"));
+ throw new Error(`missing smoke output: ${needle}`);
+ }
+}
+
+console.log("[openvibe] JS core smoke passed");
+for (const line of messages) console.log(line);
+NODE_SMOKE
+chmod +x tools/smoke-js-core-node.mjs
+
+echo "[openvibe] run JS core smoke test"
+node tools/smoke-js-core-node.mjs
+
+echo "[openvibe] apply SDK patch"
+tools/apply-openvibe-sdk.sh
+
+if [[ "$RUN_BUILD" = "1" ]]; then
+ echo "[openvibe] build SDK"
+ tools/build-sdk-linux.sh 2>&1 | tee "$HOME/ov-build.log"
+
+ echo "[openvibe] setup OpenVibe bin"
+ tools/setup-openvibe-bin.sh
+
+ echo
+ echo "[openvibe] next phase build complete."
+ echo
+ echo "Runtime test:"
+ echo " OPENVIBE_SRCDS_MAP_DELAY=3 tools/dev-up.sh"
+ echo
+ echo "Server console tests:"
+ echo " ov_js_status"
+ echo " ov_js_cmd help"
+ echo " ov_js_cmd smoke"
+ echo " ov_js_cmd timer_smoke"
+ echo " ov_js_cmd hub_status"
+ echo
+ echo "Client tests:"
+ echo " connect 127.0.0.1:27015"
+ echo " say !help"
+ echo " say !js"
+ echo " say !hp"
+ echo " say !players"
+ echo " say !where"
+else
+ echo "[openvibe] skipped SDK build because RUN_BUILD=0"
+fi
new file mode 100755
new file mode 100755
@@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
+import fs from "node:fs";
+import vm from "node:vm";
+
+let fakeTime = 0;
+const messages = [];
+
+function player() {
+ return {
+ userId: 1,
+ entIndex: 1,
+ steamId: () => "STEAM_0:1:1",
+ name: () => "SmokePlayer",
+ health: () => 87,
+ setHealth: () => undefined,
+ chat: (msg) => messages.push(`[chat] ${msg}`),
+ team: () => 0,
+ setTeam: () => undefined,
+ runCommand: (cmd) => messages.push(`[runCommand] ${cmd}`)
+ };
+}
+
+const context = vm.createContext({
+ console,
+ globalThis: {},
+ OV: {
+ log: (msg) => messages.push(`[log] ${msg}`),
+ warn: (msg) => messages.push(`[warn] ${msg}`),
+ error: (msg) => messages.push(`[error] ${msg}`),
+ getMode: () => "hub",
+ getMapName: () => "ov_hub",
+ time: () => fakeTime,
+ broadcast: (msg) => messages.push(`[broadcast] ${msg}`),
+ players: () => [player()],
+ playerByUserId: () => player(),
+ serverCommand: (cmd) => messages.push(`[serverCommand] ${cmd}`),
+ reward: () => undefined,
+ endMatch: () => undefined
+ }
+});
+context.globalThis = context;
+
+const files = [
+ "game/openvibe.games/js/core/hook.js",
+ "game/openvibe.games/js/core/gamemode.js",
+ "game/openvibe.games/js/bridge.js",
+ "game/openvibe.games/js/core/command.js",
+ "game/openvibe.games/js/core/timer.js",
+ "game/openvibe.games/js/gamemodes/base/server.js",
+ "game/openvibe.games/js/gamemodes/hub/server.js"
+];
+
+for (const file of files) {
+ const code = fs.readFileSync(file, "utf8");
+ vm.runInContext(code, context, { filename: file });
+}
+
+context.gamemode.call("Initialize");
+context.gamemode.call("MapInitialize", "ov_hub");
+context.gamemode.call("PlayerInitialSpawn", player());
+
+const blocked = context.gamemode.call("PlayerSay", player(), "!js");
+if (blocked !== false) throw new Error("!js did not block default chat");
+
+context.gamemode.call("ConsoleCommand", "smoke");
+context.gamemode.call("ConsoleCommand", "hub_status");
+context.gamemode.call("ConsoleCommand", "timer_smoke");
+fakeTime += 1;
+context.gamemode.call("Think");
+
+if (!messages.some((line) => line.includes("JavaScript hooks are working"))) throw new Error("missing !js response");
+if (!messages.some((line) => line.includes("OpenVibe embedded JS smoke test passed"))) throw new Error("missing smoke response");
+if (!messages.some((line) => line.includes("OpenVibe JS timer smoke fired"))) throw new Error("missing timer smoke response");
+
+console.log(messages.join("\n"));
+console.log("[openvibe-smoke] JS command/timer smoke passed");