|
VoltMod
C++23 framework for CS2 server plugins
|
A plugin is a library with a plugin.json beside its CMakeLists.txt and an App class in src/App.hpp that owns everything for one load cycle.
There is no entry macro. voltmod_add_plugin generates the entry point for <Namespace>::App, where the namespace is the plugin name with each word capitalized (admin-system is AdminSystem), and fails at configure time when src/App.hpp is missing. The generated file defines the plugin object, this module's hook dispatch pointer and VoltMod_Plugin, the one symbol the host resolves. Its descriptor carries the VoltMod version the plugin was built with and the short commit of the plugin's own repository.
Your App derives from VoltMod::Plugin. The framework builds the runtime with every service ready, constructs the App from Runtime&, and destroys it before the runtime shuts down.
| Override | Required | Called |
|---|---|---|
bool Load() | no | Once, after every member is built and the settings loaded; false aborts the load |
Map starts and chat are events, not overrides: Runtime.Map.Started carries the map name, and Runtime.Players.Said carries each chat line no menu or command took (set Blocked to keep it out of chat). Keep custom engine hooks, signals and timers in the App's own Subscriptions.
Members initialize in declaration order, so an initializer may reference only members above it. A member that subscribes or registers commands can do it in its constructor; work that can fail the load, or that acts outside the plugin (a server convar, a published service), belongs in Load. The App is destroyed before the Runtime, which is what lets its subscriptions unregister while the services they point at are still alive.
voltmod new plugin <name> writes plugins/<name>/:
| File | Holds |
|---|---|
CMakeLists.txt | one voltmod_add_plugin(<name>) call |
plugin.json | the manifest below |
src/App.hpp, src/App.cpp | the load-cycle object graph and its Load, which requires addonId and draws menus on the layout when menu.panorama is on |
src/Commands.cpp | the !ping command |
src/Config.hpp | the settings struct and ConfigManager |
configs/settings.jsonc | operator settings |
README.md | what the plugin does, its commands and settings |
translations/en.json | player-facing text |
panorama/screens/<name>_menu.xml.j2, .css.j2 | the menu layout; voltmod build renders it and Ui/<Pascal>Menu.hpp |
content/ | workshop sources: models, particles, sounds for the CS2 Workshop Tools |
Add .cpp files anywhere under src/; voltmod_add_plugin globs them.
The manifest is hand-written and lives beside CMakeLists.txt. CMake reads name and version from it at configure time; the host reads the copy installed at addons/voltmod/plugins/<name>/plugin.json. An unknown key is an error and the plugin is refused.
| Key | Type | Default | Meaning |
|---|---|---|---|
$schema | string | none | Points editors at the plugin schema, so they complete and check keys. The host ignores it. |
name | string | required | The plugin's directory under addons/voltmod/plugins/ and its CMake target. All three must match. |
version | string | required | Its version. volt list prints it with the commit the plugin was built from: v1.0.0 (2dfd824). |
logTag | string | name | The prefix the host puts in front of every log line from this plugin. |
description | string | "" | One line, printed after the version by volt list. |
author | string | "" | Credit. The host does not print it. |
website | string | "" | The plugin's page or repository. Credit, like author. |
license | string | "" | An SPDX id such as MIT, or proprietary. Credit only. |
logLevel | string | "info" | The level the plugin starts at: info, warn or error. volt log <name> <level> changes it until the next load; another value refuses the plugin. |
dependencies | string[] | [] | Plugins this one is refused without. |
optionalDependencies | string[] | [] | Plugins it is better with; never a reason to refuse it. |
database | object | none | migrations, header and namespace for voltmod database header, relative to the plugin directory. The host ignores it. |
Neither list decides load order. dependencies decides whether the plugin loads at all and what a reload takes down with it; see Running the host.
The connection lifecycle is not an override. Subscribe to Runtime.Players.Connected, .FullyConnected, .SettingsChanged and .Disconnected; see Players, targeting, and actions. Custom hooks are in Movement, teleports, damage, weapon drops, and server commands, typed game events in ConVars and game events.
runtime.LoadReport records what failed while the plugin loaded. The runtime checks its own services there when it is built; add a check with the VoltMod::Status your own work returns.
Optional continues without that feature. Required is for work the plugin cannot run without: the framework hands the first required failure to the host as <name>: <reason>, which the host logs as the refusal. It checks once the App is built, before Load, and again when Load returns false. Both return whether the check passed. After the load the framework logs Load took N ms with one line per failure. Work that cannot fail needs no check.
The standard prelude - settings as a required check, then translations - is the Config member's initializer:
It reads addons/voltmod/plugins/<plugin>/configs/settings.jsonc and then translations. Pass {.SettingsFile = "configs/other.jsonc"} or {.Translations = false} as the third argument to change either. A broken file leaves the defaults in place, and the plugin is refused with Configuration: <path>: <reason> before Load runs, so members built below it never act on them. See Configuration.
runtime.Status combines named diagnostic sections into one report. The framework registers build, load and uptime. Add your own in Load and expose the report as a console command:
my_status prints text, my_status json emits one STATUS_JSON {...} line for RCON tooling, and volt status <name> prints the same JSON from the host. The top-level healthy value is the predicate's answer, or true when there is no predicate. The command unregisters on unload.
Keep sections compact - counts and names, not full lists - because RCON console capture truncates long responses. A section capturing this must live on an object the Runtime outlives.
Log::Info, Log::Warn and Log::Error from <VoltMod/Core/Log.hpp> format with std::format and hand the line to the host, which prefixes the plugin's logTag. The host also sets the minimum level, so a line below it is never formatted.
Nothing survives the App's destructor, so a volt reload starts from clean state. Cleanup belongs in a member destructor or in a VoltMod::Subscription held beside the state its handler captures:
Events, game events, scheduler timers and scoped hooks return a [[nodiscard]] Subscription that unregisters on destruction; dropping a scheduler subscription cancels the timer. Draining a database or withdrawing a published interface belongs in the App destructor. Commands are owned by CommandManager for the load cycle and need no cleanup.
Whatever the plugin still holds when its view closes, the host takes back and logs:
Each of those is a bug in the plugin: something outlived the App that built it. Command names are different: the host removes them with the plugin and says nothing.
voltmod_add_plugin defines an install component named after the plugin. Staged into a server's game/csgo, the tree is:
A plugin has no bin directory of its own. runtime.PluginFile("data/x") builds addons/voltmod/plugins/<name>/data/x for any file the plugin reads at run time. Put files an operator tunes under configs/ and files the plugin ships under data/.
Compiled workshop files the server needs, such as models, particles, sound events or an override of a game file like scripts/weapons.vdata_c, go under server-assets/. While the plugin is loaded, the host mounts that folder ahead of the game's own VPKs, which a loose copy under game/csgo would lose to. The game reads weapon subclasses when a map loads, so a plugin loaded mid-map has them from the next map.
voltmod install <name> stages and merges both trees; with no name, every plugin's. By hand:
then copy dist/addons into game/csgo, leaving operator-edited settings alone, and put Game csgo/addons/voltmod directly above Game csgo in gameinfo.gi so the engine finds the loader. voltmod serve and voltmod run add that line themselves.