|
VoltMod
C++23 framework for CS2 server plugins
|
The loader the engine runs as server_valve starts the host. The host installs the engine hooks once, loads every plugin it finds under addons/voltmod/plugins/*/plugin.json, and offers each engine event to them in load order.
To keep a plugin off one server, move its directory to addons/voltmod/plugins/disabled/<name> and restart. The host does not load it there, and voltmod install skips it.
Run these on the server console or over RCON.
| Command | Does |
|---|---|
volt version | Prints VoltMod <version> (<commit>): the release and the commit the host was built from |
volt list | Prints the volt version line, then N plugin(s), in load order: and one <name> v<version> (<commit>) - <description> line each, or No plugins are loaded.; then N refused: and one <name> - <reason> line per plugin the last attempt refused, until a later load of it succeeds |
volt status | Prints <name>: <status JSON> for every loaded plugin |
volt status <name> | Prints that one line (see Writing a plugin for the sections) |
volt load <name> | Loads an installed plugin that is not loaded |
volt unload <name> | Unloads a plugin, unless a loaded plugin requires it |
volt reload <name> | Unloads the plugin and everything that requires it, then loads them all again |
volt log <name> <level> | Sets that plugin's minimum log level - info, warn or error - and prints <name> now logs <level> and above. |
load, unload and reload do not run inside the command. They answer ‘Queued <action> of ’<name>' for the next frame.` and run at the start of the next frame, so a plugin is never torn down from inside one of its own callbacks. A reload reads the manifests again, so a rebuilt plugin may declare different dependencies than the one going down.
volt itself is reserved before any plugin loads, so a plugin trying to register a command by that name is refused rather than racing the host for it. The same applies to any command two plugins both want:
dependencies names the plugins this one cannot run without; optionalDependencies names the ones it is better with and runs fine without. Neither decides load order. Plugins load alphabetically, and one plugin reaches another by asking runtime.Exchange for its interface at the moment it needs it, which answers null when that plugin is not loaded. Engine events, and console commands offered until one plugin consumes them, follow that same alphabetical order.
dependencies | optionalDependencies | |
|---|---|---|
| Not installed | this plugin is refused | ignored |
| Installed but refused | this plugin is refused | ignored |
volt unload on it | refused while this plugin is loaded | allowed |
volt reload on it | this plugin goes down and comes back with it | untouched |
So a plugin that declares "dependencies": ["admin-system"] does not load without it and comes back with it on a reload, in no particular order within that group. Reloading a plugin nothing requires touches only that plugin.
volt unload on something still required names what is in the way:
volt load on a plugin whose required dependency is not loaded refuses the same way:
Every plugin is its own library with its own runtime and its own allocator. The host keeps one service table for the process, and VoltMod::ServiceExchange is the typed view of it. Share behavior through an interface rather than handing out a manager or a framework object.
Name T explicitly in Publish<T>, so the stored pointer is the interface subobject the consumer casts back to. Put a version in InterfaceName and bump it whenever the vtable or a parameter's meaning changes: a stale consumer then gets nullptr instead of a mismatched vtable.
Several providers of one interface pass a key: Publish<IMenuSection>(this, "admin"), then Get<IMenuSection>("admin").
Ask for a service where you use it and do not keep the pointer. The publisher can unload between callbacks, never inside one, so a pointer fetched at the point of use cannot dangle before you are done with it. Never transfer ownership across the boundary, never pass an object one module's allocator owns, and let no exception escape an interface call. Use a VoltMod::ServerCommand instead when console, RCON or cfg files need the operation too.
Publish returns a Subscription that withdraws the entry when it drops. Keep it as the last member of the object it offers, so the entry goes before anything the object reaches; the host reports anything left behind at unload.
Nothing loads, no volt command. The host did not start; the console's [VoltMod] lines say why. The server needs the loader and the host in addons/voltmod/bin/<platform>/, and Game csgo/addons/voltmod directly above Game csgo in gameinfo.gi. A CS2 update removes that line; voltmod serve puts it back. Writing a plugin has the full layout.
Plugins load twice, or Metamod tries to load the host. An older install left Metamod plugin files behind. Delete addons/metamod/voltmod.vdf and any addons/metamod/<plugin>.vdf by hand.
**‘Refusing ’<dir>': plugin.json names it '<name>', and a plugin lives in the directory it is named after.** Rename the directory or thenamekey so they match. The CMake target must match too, andvoltmod_add_plugin` fails at configure time when it does not.
A refusal naming an unknown key. The manifest is read strictly: a key that is not in the table in Writing a plugin is an error, and so is a wrong value type. The reason names the offending key with its line and column.
**‘Refusing ’<name>': it was built for VoltMod X and the host is Y; rebuild it.`** The plugin and the host come from different framework versions. Rebuild the plugin and install both from the same build.
**‘Refusing ’<name>': this plugin was built against another schema layout (plugin ..., host ...); rebuild it against this VoltMod.** Schema offsets are baked into each plugin at build time and the host checks its own copy once per process. Same fix: rebuild against this host. The related the host found schema drift; its log names every fieldmeans the game moved under a host that is otherwise fine - regenerate withvoltmod framework schemagen` and rebuild.
**‘Refusing ’<name>': requires '<dep>', which is not installed** or **... which the host refused`.** Install the dependency, or fix why it was refused: every refusal upstream refuses everything below it.
**‘Refusing ’<name>': installed more than once; each plugin directory needs its own plugin name.** Two directories underaddons/voltmod/plugins/carry manifests with the samename`.
A plugin loads but a feature is missing. The load summary logs optional <feature>: <reason> for each engine feature that could not bind, and volt status <name> repeats it in the load section's failed list. That usually means gamedata went stale after a game update; see Gamedata and schema.