|
VoltMod
C++23 framework for CS2 server plugins
|
Gamedata says where engine code and untyped fields live. The schema says where entity fields live. Both are resolved once and read from there.
| says where | source | checked | |
|---|---|---|---|
gamedata/gamedata.jsonc | functions, globals, vtable slots, byte offsets | hand-maintained | by the host at startup, per entry |
gamedata/schema/server.<platform>.json | entity field offsets | dumped from the running engine | at load, whole layout |
The host reads addons/voltmod/gamedata/gamedata.jsonc and scans for every entry once, before any plugin loads. VoltMod::Bindings gives those locations C++ types:
Each plugin module calls Bindings::Bind before it builds the runtime, taking every member in one pass from a VoltMod::GameDataLookup over what the host already resolved. Services then hold const Bindings&, so no call path does a string lookup. No plugin reads the file or scans memory itself: a signature a game update broke costs one scan and one error line for the whole server.
Bindings::Failures lists key: reason for every member the last Bind left unbound.
Four sections, named for what they bind. A key is the engine's own name for the symbol, or Class::member, so it can be checked against upstream gamedata and against the binary. Each key belongs to exactly one section.
Wildcard bytes are ? or ??. A class name is the top-level RTTI name, with no namespace or template. An offset entry is either per-platform numbers or a class + base pair read from RTTI at load, never both.
A script entry has no platform columns. The host reads class's VScript description, searches its bindings and then its bases' for the name, and takes the function, or for a vtable entry the slot the binding dispatches through. A VScript binding can vanish in an update, so use one only where the binding calls the engine function itself.
gamedata.schema.json in the framework repository has additionalProperties: false everywhere, and the file's $schema loads it from GitHub, so an editor flags a typo before the server sees it. Only gamedata.jsonc ships; the schema is never copied to a server or into the package.
The file is read strictly: an unknown key, a wrong value type, or a section that is not an object rejects the whole file, and the host then has nothing to serve.
Otherwise every entry is resolved and each failure is logged once with its reason:
script binding is not in the class or its bases, or is virtual where a function is wanted, or not virtual where a slot is;base is not in its class through RTTI, is in it more than once, is virtual, or has no vtable of its own.Each plugin's GameData load check then lists the failures touching its own members, and each feature reports its own through Available().
A pattern is validated by matching exactly once. A vtable index or a byte offset cannot be validated that way, so when build.server does not match the running server the load warns that those columns are unchecked on this build. The log records each vtable slot's code as key=module+offset, to match a crash dump against a binding.
When every entry resolves, the host writes addons/voltmod/gamedata/dumps/resolved.json: the server build, module-relative addresses for functions, globals and class tables, plus slot indices and offsets. It is written once per server build, and a failed write does not fail the load. Keep the record from a known-good build to diff against after an update.
Schema offsets need no gamedata. voltmod framework schemagen bakes them into generated accessors, and the host verifies the whole generated layout against the live schema once:
schemagen reads gamedata/schema/manifest.json plus a dump and writes include/VoltMod/Schema/Generated/ and src/Schema/Generated/<platform>/. The dump comes from the engine's network serializers, which exist only while a map runs, so the host writes addons/voltmod/gamedata/dumps/schema.json at map start and skips the write when the file on disk already carries the running game build.
Windows and Linux lay entity classes out differently. Each platform has its own committed baseline in gamedata/schema/server.<platform>.json and its own generated sources, so run schemagen once per platform. Re-rendering a platform from its baseline needs no server:
Field access on a wrapper is what Entities and players describes.
The offsets are compiled into each plugin's own copy of the SDK, so the host can only vouch for a plugin built from the same generated layout. schemagen emits GeneratedLayoutStamp(), a hash of class names and sizes, field names, offsets and sizes: regenerating an unchanged layout keeps the value, and a moved field changes it. Each plugin compares its stamp with the host's before taking the host's answer, and refuses the load when they differ:
The plugin binary and voltmod.dll came from different builds of the framework. Rebuild the plugin against the VoltMod the host was built from and install both together.
When the stamps match but the live schema moved a field, the load aborts with:
and the host's log names each one:
voltmod doctor --server <dir> says when the server is behind Steam, and when gamedata was checked on another build than the server runs. voltmod serve puts back the VoltMod line an update removes from gameinfo.gi.
A dump needs a running map, so a cold start refuses every plugin before the first map loads. Update day is: start the server, let the plugins refuse, let a map load so the host writes the dump, run voltmod framework schemagen, review the git diff of the generated code, rebuild.
Gamedata is repaired separately, and offline:
voltmod framework gamedata fetch downloads the new build's server and engine2 binaries for both platforms into ~/.voltmod/cs2-builds/<build>/<platform> (CS2_BUILD_ARCHIVE moves it). It also files the local server's resolved.json under the archived build it names, as resolved.<platform>.json, so run it before updating the server. Steam serves only the current build, so this archive is the only way to compare an update with the build before it.voltmod framework gamedata check --game-dir ~/.voltmod/cs2-builds/<build>/<platform> reports which functions and globals patterns no longer match those binaries, and why. It needs no server.voltmod framework gamedata check --fix repairs what it can, then read the diff.script entries need no check: the load log says when a binding is gone.build.server and build.verified in the same change.check takes --game-dir (default CS2_SERVER_PATH) and --platform. The platform otherwise follows whichever binaries that directory holds.
What --fix will and will not do:
Common drift points:
| Entry | Section | Used by | Drift symptom |
|---|---|---|---|
CPlayer_MovementServices::RunCommand | vtables | VoltMod::Movement | Crash on the first movement tick, unless the slot check catches it |
CBaseEntity::Teleport | vtables | VoltMod::Teleport | Subscribing to Teleported is refused; Teleport::Available says why |
CServerSideClient::ProcessRespondCvarValue | vtables | VoltMod::ClientConVars | ClientConVars::Available fails; queries unavailable |
INetworkMessageProcessingPreFilter::FilterMessage | vtables (base) | VoltMod::ScreenManager | Presses never arrive; a stale index hooks a different filter |
CUserCmd::CSGOUserCmdPB | offsets | VoltMod::Movement | Missing: Valid=false. Stale: garbage viewangles and buttons |
GameEntitySystem | offsets | VoltMod::EntitySystem | Stale: the pointer is not a CGameEntitySystem, so lookups return nothing |
CUserCmdBase::cmdNum | offsets | PlayerInput::CommandNumber | Missing: falls back to legacy_command_number, which live clients leave at 0 |
CServerSideClientBase::m_nClientSlot | offsets | ClientConVars, screen presses | Stale: a client's answer is attributed to the wrong player |
CheckTransmitPlayerSlot | offsets | VoltMod::Visibility | Stale: the wrong recipient is filtered |
CNetworkGameServer::ReplyConnection | functions | VoltMod::MultiAddonManager | Add logs the reason in the host and requires nothing |
CNetworkGameServer::m_szAddons | offsets | VoltMod::MultiAddonManager | Stale: it no longer holds what GetAddonName returns, so each reply logs it and mounts nothing |
CSource2Server::g_GameEventManager | globals | VoltMod::GameEvents | Events do not fire and center HTML does not display |
The host finds a class vtable by RTTI on Windows and by _ZTV symbol - falling back to the mapped module's Itanium RTTI - on Linux. A class name must therefore be the top-level RTTI name exactly; a nested or namespaced name resolves to nothing and the entry does not bind.