openvibers/source.openvibe.games Commits

Add JS command registry and timers

CO Copilot committed 29 Jun 2026 · 0ae4bd36ec · 12 files, +2296 −211


Changes

ddiff --git a/game/openvibe.games/js/bridge.js b/game/openvibe.games/js/bridge.js
iindex 18b0b88..d943d10 100644
---- a/game/openvibe.games/js/bridge.js
++++ b/game/openvibe.games/js/bridge.js
@@@ -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");
++ }
++})();
ddiff --git a/game/openvibe.games/js/gamemodes/base/server.js b/game/openvibe.games/js/gamemodes/base/server.js
iindex 787eb56..c5cc71f 100644
---- a/game/openvibe.games/js/gamemodes/base/server.js
++++ b/game/openvibe.games/js/gamemodes/base/server.js
@@@ -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);
++})();
ddiff --git a/game/openvibe.games/js/gamemodes/deathrun/server.js b/game/openvibe.games/js/gamemodes/deathrun/server.js
iindex 8ff1e6c..b13432e 100644
---- a/game/openvibe.games/js/gamemodes/deathrun/server.js
++++ b/game/openvibe.games/js/gamemodes/deathrun/server.js
@@@ -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);
++})();
ddiff --git a/game/openvibe.games/js/gamemodes/fortwars/server.js b/game/openvibe.games/js/gamemodes/fortwars/server.js
iindex f907888..db5c07c 100644
---- a/game/openvibe.games/js/gamemodes/fortwars/server.js
++++ b/game/openvibe.games/js/gamemodes/fortwars/server.js
@@@ -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);
++})();
ddiff --git a/game/openvibe.games/js/gamemodes/hub/server.js b/game/openvibe.games/js/gamemodes/hub/server.js
iindex a0e0693..e2aebdd 100644
---- a/game/openvibe.games/js/gamemodes/hub/server.js
++++ b/game/openvibe.games/js/gamemodes/hub/server.js
@@@ -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);
++})();
ddiff --git a/game/openvibe.games/js/gamemodes/prophunt/server.js b/game/openvibe.games/js/gamemodes/prophunt/server.js
iindex 55a0612..8eebee6 100644
---- a/game/openvibe.games/js/gamemodes/prophunt/server.js
++++ b/game/openvibe.games/js/gamemodes/prophunt/server.js
@@@ -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);
++})();
ddiff --git a/game/openvibe.games/js/gamemodes/traitortown/server.js b/game/openvibe.games/js/gamemodes/traitortown/server.js
iindex 86fee46..5ba0c47 100644
---- a/game/openvibe.games/js/gamemodes/traitortown/server.js
++++ b/game/openvibe.games/js/gamemodes/traitortown/server.js
@@@ -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);
++})();
ddiff --git a/sdk/openvibe/shared/ov_js_runtime.cpp b/sdk/openvibe/shared/ov_js_runtime.cpp
iindex c35d33e..9bbb640 100644
---- a/sdk/openvibe/shared/ov_js_runtime.cpp
++++ b/sdk/openvibe/shared/ov_js_runtime.cpp
@@@ -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;
}
ddiff --git a/tools/fix-js-gamemode-scope-and-smoke.sh b/tools/fix-js-gamemode-scope-and-smoke.sh
nnew file mode 100755
iindex 0000000..e3c3256
---- /dev/null
++++ b/tools/fix-js-gamemode-scope-and-smoke.sh
@@@ -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"
ddiff --git a/tools/fix-loadcorefiles-js-phase.sh b/tools/fix-loadcorefiles-js-phase.sh
nnew file mode 100755
iindex 0000000..dcaa5fe
---- /dev/null
++++ b/tools/fix-loadcorefiles-js-phase.sh
@@@ -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"
ddiff --git a/tools/next-phase-js-core-commands-timers.sh b/tools/next-phase-js-core-commands-timers.sh
nnew file mode 100755
iindex 0000000..680ddb8
---- /dev/null
++++ b/tools/next-phase-js-core-commands-timers.sh
@@@ -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
ddiff --git a/tools/smoke-js-core-node.mjs b/tools/smoke-js-core-node.mjs
nnew file mode 100755
iindex 0000000..80304c5
---- /dev/null
++++ b/tools/smoke-js-core-node.mjs
@@@ -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");