|
VoltMod
C++23 framework for CS2 server plugins
|
Subscribe with On<T> and a typed event from <VoltMod/Events/EventTypes.hpp>. Every event the game defines has a struct, generated from its .gameevents files; each names the engine event in EventName and decodes its fields in From.
There is no string subscription API, and none is needed. The names are generated, never chosen:
| Engine | C++ |
|---|---|
event bomb_planted | struct BombPlanted |
key dmg_health, oldteam | DmgHealth, Oldteam |
player key userid | Slot, the player the event is about |
any other player key, such as attacker | AttackerSlot |
team, oldteam, hitgroup | VoltMod::Team, VoltMod::HitGroup |
On<T> never calls a handler with an invalid Slot, so a handler indexes per-slot state without a guard; an optional party such as AttackerSlot stays -1 when absent. player_pawn and ehandle fields are left out as // skipped: comments until a plugin needs one.
After a game update, regenerate the pair with uv run voltmod framework eventgen --server C:/cs2-server (it reads the VPKs under the server, like schemagen reads its dump) and commit the result. bullet_impact is written by hand in Events/BulletImpact.hpp, which the generated header includes.
CreateEvent / FireEvent / FreeEvent create and fire events; the center-HTML transport is built on exactly that.
Call On<T> during load and keep the returned Subscription. The engine resets its listener table at map startup and the framework reattaches every listener afterwards, so nothing has to be re-subscribed. Handlers may subscribe or unsubscribe during dispatch; a new handler starts with the next event. volt reload detaches the old listeners before the new load registers them.
PlayerDeath::Penetrated counts surfaces the killing bullet crossed, so anything above zero is a wallbang.
VoltMod::BulletImpact fires once per bullet landing, but the engine truncates userid to one byte, so ShooterSlot is best effort and may be -1 or name the wrong player. Correlate impacts by tick and use TruncatedUserId only to disambiguate candidates.
GetClientLegacyListener(slot) returns the client's engine-side listener, or nullptr when the slot has no client or the GetLegacyGameEventListener signature did not resolve. Firing an event at it delivers to that client alone.
A vanilla client subscribes only to what its HUD needs, so unexpected subscriptions can indicate injected client code. false also means unavailable, so check GetClientLegacyListener first when that distinction matters.
VoltMod::ConVar handles bool, int, float and std::string. Resolve the handle once and keep it; handles survive map changes, and an unresolved one is falsy, reads as T{} and rejects writes.
ConVarChange's string views borrow engine storage and live only for the handler.
| Call | What it does |
|---|---|
Set(value) | Queues a cfg-style console write. Callbacks fire and FCVAR_REPLICATED values reach clients, so client prediction cannot be left on the old value. |
SetFor(slot, value) | Changes one client's replicated view, leaving the server and other clients alone. |
RawScope(value) | Writes storage with no callbacks and nothing networked, restoring the previous value when the scope dies. Not available for std::string. |
A client's connect and map-change snapshots restore the server value, so re-send a SetFor override from a PlayerSpawn handler to keep it sticky. For a hook pair, store the RawScope in a member and release it in post; the handle must outlive the scope.
VoltMod::ConVarOverrides saves the original before the first write and restores only what it changed:
Later Set calls reapply the override without replacing the saved original. Reapply after map resets.
VoltMod::Map validates map names and changes level. It holds no map list: which maps a server offers is operator configuration, so that list belongs to the plugin.
IsValid answers only for plain names. A workshop map is not mounted until it loads, so there is nothing to probe; check those by other means or accept the engine's own failure.
maps.Current() is the map the server is running, captured from StartupServer, or read from the engine globals after a mid-map load. Both change calls take effect immediately, so schedule the call rather than delaying inside a listener when players should read an announcement first.