openvibers/source.openvibe.games Commits
Phase 3+4: JS require(), addon system, npm module resolver
Changes
nnew file mode 100644@@@ -0,0 +1 @@++{ "name": "Hello World", "author": "OpenVibe", "entry": { "shared": "shared.js", "server": "server.js", "client": "client.js" } }nnew file mode 100644@@@ -0,0 +1 @@++console.log("hello-world client.js loaded");nnew file mode 100644@@@ -0,0 +1,9 @@++// server realm: use an npm module + register a chat command++var leftpad = require("ov-leftpad");++OV.log("hello-world server.js loaded; leftpad('7',3,'0')=" + leftpad("7", 3, "0"));++if (globalThis.command) {++ command.add("hello", "Addon demo command", function ({ ply, reply }) {++ reply(ply, "Hello from the JS addon system! padded=" + leftpad("42", 5, "0"));++ return false;++ });++}nnew file mode 100644@@@ -0,0 +1,3 @@++// runs in both realms++module.exports.tag = "[hello-world]";++OV.log("hello-world shared.js loaded, isServer=" + OV.isServer());@@@ -1,5 +1,37 @@++// OpenVibe C++ <-> JS bridge bootstrap.++//++// The C++ core loader only loads a fixed list of files (hook, gamemode, bridge,++// command, timer). We use this always-loaded file to bootstrap the higher-level++// platform that the C++ list doesn't know about — the CommonJS module system++// (require/npm) and the addon loader — by reading and evaluating them through++// the OV.readFile file bridge. This keeps those systems as normal on-disk files++// that can be edited/hotloaded without rebuilding the game DLLs.(function () {if (globalThis.OV && typeof OV.log === "function") {OV.log("bridge.js loaded");}++++ if (!globalThis.OV || typeof OV.readFile !== "function") {++ // Older DLL without the file bridge — platform features unavailable.++ return;++ }++++ function bootstrap(path) {++ try {++ var code = OV.readFile(path);++ if (code == null) { OV.warn("bridge: missing " + path); return; }++ // Global eval so the file's top-level (function(){...})() runs in global scope.++ (0, eval)(code + "\n//# sourceURL=" + path);++ } catch (e) {++ OV.error("bridge: failed to load " + path + ": " + (e && e.message ? e.message : e));++ }++ }++++ bootstrap("js/core/module.js"); // defines globalThis.require++ bootstrap("js/core/addon.js"); // defines globalThis.Addon++++ // NOTE: Addon.loadAll() is intentionally NOT called here. bridge.js loads++ // before command.js/timer.js in the C++ core order, so addons that register++ // commands or timers would find them missing. base/server.js (loaded after++ // all core files, in both realms) triggers Addon.loadAll() instead.})();@@@ -99,4 +99,17 @@gamemode.setBase(GM);gamemode.set(GM, { base: true });++++ // Load addons now that all core systems (require, hook, command, timer,++ // gamemode) are available. Runs in both realms; Addon.loadAll() picks the++ // right per-realm entry files via OV.isServer(). The active gamemode file++ // loads immediately after this, so addon hooks are registered first and can++ // override gamemode behavior — matching GMod's addon-over-gamemode ordering.++ try {++ if (globalThis.Addon && typeof Addon.loadAll === "function") {++ Addon.loadAll();++ }++ } catch (e) {++ if (globalThis.OV && OV.error) OV.error("base: addon load failed: " + (e && e.message ? e.message : e));++ }})();nnew file mode 100644@@@ -0,0 +1,6 @@++module.exports = function leftpad(str, len, ch) {++ str = String(str);++ ch = ch || " ";++ while (str.length < len) str = ch + str;++ return str;++};nnew file mode 100644@@@ -0,0 +1 @@++{ "name": "ov-leftpad", "version": "1.0.0", "main": "index.js" }@@@ -59,6 +59,11 @@ static JSValue OVJS_getMode(JSContext *ctx, JSValueConst thisVal, int argc, JSVareturn JS_NewString(ctx, g_OVRuntime ? g_OVRuntime->GetMode() : "unknown");}++static JSValue OVJS_isServer(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)++{++ return (g_OVRuntime && g_OVRuntime->IsServerRealm()) ? JS_TRUE : JS_FALSE;++}++static JSValue OVJS_getMapName(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv){return JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");@@@ -211,7 +216,9 @@ static JSValue OVJS_fileExists(JSContext *ctx, JSValueConst thisVal, int argc, Jconst char *path = JS_ToCString(ctx, argv[0]);if (!path) return JS_FALSE;-- bool exists = OVJS_IsSafeModPath(path) && filesystem->FileExists(path, "MOD");++ bool exists = OVJS_IsSafeModPath(path)++ && filesystem->FileExists(path, "MOD")++ && !filesystem->IsDirectory(path, "MOD");JS_FreeCString(ctx, path);return exists ? JS_TRUE : JS_FALSE;}@@@ -262,6 +269,7 @@ static const JSCFunctionListEntry OVFuncs[] =JS_CFUNC_DEF("warn", 1, OVJS_warn),JS_CFUNC_DEF("error", 1, OVJS_error),JS_CFUNC_DEF("getMode", 0, OVJS_getMode),++ JS_CFUNC_DEF("isServer", 0, OVJS_isServer),JS_CFUNC_DEF("getMapName", 0, OVJS_getMapName),JS_CFUNC_DEF("time", 0, OVJS_time),JS_CFUNC_DEF("players", 0, OVJS_players),@@@ -31,6 +31,7 @@ public:JSContext *Context() { return nullptr; }const char *GetMode() const { return m_szMode; }++ bool IsServerRealm() const { return m_bServerRealm; }private:bool m_bServerRealm = false;@@@ -57,6 +58,7 @@ public:JSContext *Context() { return m_pCtx; }const char *GetMode() const { return m_szMode; }++ bool IsServerRealm() const { return m_bServerRealm; }private:void PrintException(const char *pszWhere);
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "name": "Hello World", "author": "OpenVibe", "entry": { "shared": "shared.js", "server": "server.js", "client": "client.js" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+console.log("hello-world client.js loaded");
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+// server realm: use an npm module + register a chat command
+var leftpad = require("ov-leftpad");
+OV.log("hello-world server.js loaded; leftpad('7',3,'0')=" + leftpad("7", 3, "0"));
+if (globalThis.command) {
+ command.add("hello", "Addon demo command", function ({ ply, reply }) {
+ reply(ply, "Hello from the JS addon system! padded=" + leftpad("42", 5, "0"));
+ return false;
+ });
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+// runs in both realms
+module.exports.tag = "[hello-world]";
+OV.log("hello-world shared.js loaded, isServer=" + OV.isServer());
@@ -1,5 +1,37 @@
@@ -1,5 +1,37 @@
+// OpenVibe C++ <-> JS bridge bootstrap.
+//
+// The C++ core loader only loads a fixed list of files (hook, gamemode, bridge,
+// command, timer). We use this always-loaded file to bootstrap the higher-level
+// platform that the C++ list doesn't know about — the CommonJS module system
+// (require/npm) and the addon loader — by reading and evaluating them through
+// the OV.readFile file bridge. This keeps those systems as normal on-disk files
+// that can be edited/hotloaded without rebuilding the game DLLs.
(function () {
(function () {
if (globalThis.OV && typeof OV.log === "function") {
if (globalThis.OV && typeof OV.log === "function") {
OV.log("bridge.js loaded");
OV.log("bridge.js loaded");
}
}
+
+ if (!globalThis.OV || typeof OV.readFile !== "function") {
+ // Older DLL without the file bridge — platform features unavailable.
+ return;
+ }
+
+ function bootstrap(path) {
+ try {
+ var code = OV.readFile(path);
+ if (code == null) { OV.warn("bridge: missing " + path); return; }
+ // Global eval so the file's top-level (function(){...})() runs in global scope.
+ (0, eval)(code + "\n//# sourceURL=" + path);
+ } catch (e) {
+ OV.error("bridge: failed to load " + path + ": " + (e && e.message ? e.message : e));
+ }
+ }
+
+ bootstrap("js/core/module.js"); // defines globalThis.require
+ bootstrap("js/core/addon.js"); // defines globalThis.Addon
+
+ // NOTE: Addon.loadAll() is intentionally NOT called here. bridge.js loads
+ // before command.js/timer.js in the C++ core order, so addons that register
+ // commands or timers would find them missing. base/server.js (loaded after
+ // all core files, in both realms) triggers Addon.loadAll() instead.
})();
})();
@@ -99,4 +99,17 @@
@@ -99,4 +99,17 @@
gamemode.setBase(GM);
gamemode.setBase(GM);
gamemode.set(GM, { base: true });
gamemode.set(GM, { base: true });
+
+ // Load addons now that all core systems (require, hook, command, timer,
+ // gamemode) are available. Runs in both realms; Addon.loadAll() picks the
+ // right per-realm entry files via OV.isServer(). The active gamemode file
+ // loads immediately after this, so addon hooks are registered first and can
+ // override gamemode behavior — matching GMod's addon-over-gamemode ordering.
+ try {
+ if (globalThis.Addon && typeof Addon.loadAll === "function") {
+ Addon.loadAll();
+ }
+ } catch (e) {
+ if (globalThis.OV && OV.error) OV.error("base: addon load failed: " + (e && e.message ? e.message : e));
+ }
})();
})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+module.exports = function leftpad(str, len, ch) {
+ str = String(str);
+ ch = ch || " ";
+ while (str.length < len) str = ch + str;
+ return str;
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "name": "ov-leftpad", "version": "1.0.0", "main": "index.js" }
@@ -59,6 +59,11 @@ static JSValue OVJS_getMode(JSContext *ctx, JSValueConst thisVal, int argc, JSVa
@@ -59,6 +59,11 @@ static JSValue OVJS_getMode(JSContext *ctx, JSValueConst thisVal, int argc, JSVa
return JS_NewString(ctx, g_OVRuntime ? g_OVRuntime->GetMode() : "unknown");
return JS_NewString(ctx, g_OVRuntime ? g_OVRuntime->GetMode() : "unknown");
}
}
+static JSValue OVJS_isServer(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
+{
+ return (g_OVRuntime && g_OVRuntime->IsServerRealm()) ? JS_TRUE : JS_FALSE;
+}
+
static JSValue OVJS_getMapName(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
static JSValue OVJS_getMapName(JSContext *ctx, JSValueConst thisVal, int argc, JSValueConst *argv)
{
{
return JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");
return JS_NewString(ctx, gpGlobals ? STRING(gpGlobals->mapname) : "");
@@ -211,7 +216,9 @@ static JSValue OVJS_fileExists(JSContext *ctx, JSValueConst thisVal, int argc, J
@@ -211,7 +216,9 @@ static JSValue OVJS_fileExists(JSContext *ctx, JSValueConst thisVal, int argc, J
const char *path = JS_ToCString(ctx, argv[0]);
const char *path = JS_ToCString(ctx, argv[0]);
if (!path) return JS_FALSE;
if (!path) return JS_FALSE;
- bool exists = OVJS_IsSafeModPath(path) && filesystem->FileExists(path, "MOD");
+ bool exists = OVJS_IsSafeModPath(path)
+ && filesystem->FileExists(path, "MOD")
+ && !filesystem->IsDirectory(path, "MOD");
JS_FreeCString(ctx, path);
JS_FreeCString(ctx, path);
return exists ? JS_TRUE : JS_FALSE;
return exists ? JS_TRUE : JS_FALSE;
}
}
@@ -262,6 +269,7 @@ static const JSCFunctionListEntry OVFuncs[] =
@@ -262,6 +269,7 @@ static const JSCFunctionListEntry OVFuncs[] =
JS_CFUNC_DEF("warn", 1, OVJS_warn),
JS_CFUNC_DEF("warn", 1, OVJS_warn),
JS_CFUNC_DEF("error", 1, OVJS_error),
JS_CFUNC_DEF("error", 1, OVJS_error),
JS_CFUNC_DEF("getMode", 0, OVJS_getMode),
JS_CFUNC_DEF("getMode", 0, OVJS_getMode),
+ JS_CFUNC_DEF("isServer", 0, OVJS_isServer),
JS_CFUNC_DEF("getMapName", 0, OVJS_getMapName),
JS_CFUNC_DEF("getMapName", 0, OVJS_getMapName),
JS_CFUNC_DEF("time", 0, OVJS_time),
JS_CFUNC_DEF("time", 0, OVJS_time),
JS_CFUNC_DEF("players", 0, OVJS_players),
JS_CFUNC_DEF("players", 0, OVJS_players),
@@ -31,6 +31,7 @@ public:
@@ -31,6 +31,7 @@ public:
JSContext *Context() { return nullptr; }
JSContext *Context() { return nullptr; }
const char *GetMode() const { return m_szMode; }
const char *GetMode() const { return m_szMode; }
+ bool IsServerRealm() const { return m_bServerRealm; }
private:
private:
bool m_bServerRealm = false;
bool m_bServerRealm = false;
@@ -57,6 +58,7 @@ public:
@@ -57,6 +58,7 @@ public:
JSContext *Context() { return m_pCtx; }
JSContext *Context() { return m_pCtx; }
const char *GetMode() const { return m_szMode; }
const char *GetMode() const { return m_szMode; }
+ bool IsServerRealm() const { return m_bServerRealm; }
private:
private:
void PrintException(const char *pszWhere);
void PrintException(const char *pszWhere);