NodeMod: nodemod-goldsrc
CSRetro uses a vendored, CSRetro-modified fork of nodemod-goldsrc as its TypeScript/JavaScript plugin runtime (3rdparty/nodemod/). The full source tree is committed in-tree (no submodules).
Vendored tree revision in this build: see VENDOR.md and Vendor table (VENDOR.md).
API and events (CSRetro-oriented)
NodeMod exposes a TypeScript API via @nodemod/core. Runtime layout and ./tools.sh integration are documented in NodeMod – CSRetro. Cross-component reminders (Metamod, YaPB, engine) live in Cross-component reference.
Core services (nodemodCore)
The default export from @nodemod/core groups services used by plugins:
| Service | Role |
|---|---|
cmd |
Register client/server commands, parse arguments, client command hooks. |
menu |
Built-in menu pages, pagination, key handling. |
msg |
User messages and engine message helpers. |
cvar |
Read/write engine and game CVars (wrappers around native cvars). |
file |
Virtual filesystem and path helpers. |
util |
Chat, printing, misc helpers. |
player |
Player iteration, wrappers, utilities. |
entity |
Entity wrappers, touch/use helpers. |
trace |
Traces and collision. |
events |
Thin helpers over nodemod.on (once, waitFor, throttling). |
resource |
Precache / model sound resource helpers. |
sound |
Sound helpers. |
Implementation reference: 3rdparty/nodemod/packages/core/src/index.ts.
Global nodemod object
The native binding exposes nodemod.on, nodemod.cwd, nodemod.gameDir, nodemod.players, nodemod.mapname, nodemod.frametime, etc. Type definitions: 3rdparty/nodemod/packages/core/types/index.d.ts.
Events (nodemod.on)
Subscribe with nodemod.on(eventName, handler) (or nodemodCore.events.* helpers). The authoritative list of event names and callback signatures is the generated EventCallbacks interface in:
3rdparty/nodemod/packages/core/types/events.d.ts
Event families include:
| Family | Examples (not exhaustive) |
|---|---|
| DLL / game | dllGameInit, dllSpawn, dllThink, dllClientConnect, dllClientPutInServer, dllClientCommand, dllServerActivate, dllStartFrame, dllPlayerPreThink, dllPlayerPostThink |
| Engine | engPrecacheModel, engMessageBegin, engSetModel, … (precache, messaging, sound, strings) |
| Post- hooks | postDllSpawn, postEngPrecacheModel, … (run after original game code) |
Use setMetaResult / MRES where you need to supersede or skip original behaviour (see type definitions and native examples under packages/core/src/).
Configuration and admin files (runtime)
Path (under cstrike/addons/nodemod/) |
Purpose |
|---|---|
configs/plugins.ini |
Which compiled plugins load at startup. |
configs/users.ini |
Admin user entries (used by @nodemod/admin). |
plugins/dist/*.plugin.js |
Compiled plugin output consumed at runtime. |
logs/nodemod-build.log |
Build log from the npm pipeline (when enabled). |
Vendored TypeScript sources for the admin bundle live under 3rdparty/nodemod/packages/admin/src/ (for example csr_botmenu.plugin.ts).
What is NodeMod?
NodeMod is a Metamod plugin that embeds a full Node.js runtime into the GoldSrc/Xash3D server process. This enables server plugins to be written in TypeScript or JavaScript instead of C++, with access to all engine and game DLL functions.
The original concept was created by TheEVolk (Maksim Nikiforov). The modern version used in CSRetro is a complete modernization by stevenlafl (Steven Linn), including Node.js v24, all engine/DLL bindings, npm packages, and compiled distributions.
Important: NodeMod requires Metamod to be loaded first — it is a Metamod plugin, not a standalone component.
Plugin architecture
addons/nodemod/
├── dlls/
│ └── libcsr_nodemod.so ← Metamod plugin (embeds Node.js runtime; legacy name: libnodemod.so)
├── configs/
│ └── plugins.ini ← NodeMod plugin list
└── plugins/ ← TypeScript/JS plugin workspace
├── package.json
├── node_modules/
│ └── @nodemod/
│ ├── core/ ← Type-safe engine/DLL API
│ └── admin/ ← Admin system (AMX Mod X port)
├── packages/
│ └── admin/ ← symlink → node_modules/@nodemod/admin
├── src/ ← Plugin source (.plugin.ts files)
└── dist/ ← Compiled plugin output (.plugin.js)
@nodemod/core
@nodemod/core provides the TypeScript type definitions and helper utilities for all engine and game DLL functions. It is the foundation every NodeMod plugin builds on.
Key modules: command system, menu system, player management, entity system, event handling, CVar management, file system, trace/ray system.
@nodemod/admin
@nodemod/admin is a TypeScript port of the classic AMX Mod X admin plugin suite, adapted for NodeMod. It provides:
- Admin authentication (SteamID, IP, name-based)
- Admin commands: kick, ban, slap, slay, map change, cvar control
- Voting system (map vote, kick vote)
- Player and map management menus
- Multi-language support
- Access flag system (
ADMIN_KICK,ADMIN_BAN,ADMIN_MAP, etc.)
CSRetro: csr_botmenu plugin
CSRetro ships a custom csr_botmenu.plugin.ts (part of @nodemod/admin) that provides an in-game bot management menu. After build:full or install:addons it is compiled and available via the p key binding.
Writing a plugin
All plugins use the .plugin.ts extension and must be placed in plugins/src/:
// plugins/src/myplugin.plugin.ts
import nodemodCore from '@nodemod/core';
import { BasePlugin, Plugin, PluginMetadata } from '@nodemod/admin';
class MyPlugin extends BasePlugin implements Plugin {
readonly metadata: PluginMetadata = {
name: 'My Plugin',
version: '1.0.0',
author: 'Your Name',
description: 'Example plugin'
};
constructor(pluginName: string) {
super(pluginName);
this.registerCommand('my_cmd', 0, 'Does something',
(entity, args) => nodemodCore.serverPrint('Hello!\n'));
}
}
export default MyPlugin;
After editing, rebuild with:
./tools.sh install:addons # re-runs npm install + npm run build
Building / installation
NodeMod is installed automatically as part of build:full:
./tools.sh build:full # Full pipeline: builds engine + client + NodeMod + admin npm
./tools.sh install:addons # Incremental: re-install NodeMod + rebuild TypeScript plugins
ENABLE_NODEMOD_PIPELINE=0 ./tools.sh build:full # Skip npm/TS step
If libcsr_nodemod.so (or legacy libnodemod.so) is missing when run:game or run:debug is launched, it is downloaded or built automatically (lazy install).
Engine shutdown and V8
Runtime: NodeMod and @nodemod/admin are not broken when you see [Admin] Loaded … plugins and amx_plugins lists entries — that output means the stack is alive.
Quit: Older teardown order skipped MultiIsolatePlatform::DisposeIsolate() after FreeIsolateData(), so V8::Dispose() could hit a V8 CHECK such as group->reference_count_.load() == 1 inside Meta_Detach. Current CSRetro NodeImpl::Stop() follows the same order as node::NodeMainInstance (free isolate data, then DisposeIsolate).
If you still see the assertion, you are likely on a prebuilt libnodemod.so tarball that predates the fix — rebuild/install NodeMod from source (libnode_fat.a present) or wait for a refreshed binary that includes this teardown.
Troubleshooting
| Problem | Solution |
|---|---|
libcsr_nodemod.so / libnodemod.so missing |
Run ./tools.sh install:addons; check NODEMOD_SKIP_BUILD, NODEMOD_SKIP_NATIVE, NODEMOD_SKIP_RELEASE_DL |
| V8 assertion on exit | Should be fixed in-tree (DisposeIsolate after FreeIsolateData); if it persists, replace tarball libnodemod.so with a native build — see Engine shutdown and V8 |
| npm build fails | Ensure Node.js 18+ and npm are installed; run npm install in game-test/cstrike/addons/nodemod/plugins/ |
| Plugin not loading | Check logs/nodemod-build.log; verify configs/plugins.ini entry |
meta list shows NodeMod as "not loaded" |
Check that Metamod itself is loaded (meta version); verify plugins.ini path |
Attribution
- Original concept: TheEVolk (Maksim Nikiforov)
- Complete modernization (Node.js v24, all bindings, npm, distributions): stevenlafl (Steven Linn)
- C++17 modernization and build system: SNMetamorph
- samp-node inspiration: iAmir (Amyr Aahmady)
@nodemod/adminTypeScript port: Steven Linn (stevenlafl), based on the original AMX Mod X admin plugins (OLO, AMX Mod X Development Team)
See credits for full attribution.