|
VoltMod
C++23 framework for CS2 server plugins
|
Three value types wrap a live entity, each adding to the one before it:
| Type | What it is | Where it comes from |
|---|---|---|
| VoltMod::Entity | Any entity: the CBaseEntity fields, position, teleport | runtime.Entities.Get(ref), Find, FindAll, entity.AsPawn() |
| VoltMod::Pawn | A player's body: health, armor, movement, aim, render | runtime.Entities.Pawn(slot), Pawn(ref) |
| VoltMod::Controller | A player's identity: name, money, team, kick | runtime.Entities.Controller(slot) |
The controller is the scoreboard identity and survives respawns; the pawn is the replaceable body. CBaseEntity fields read off a Controller belong to the controller entity and mean nothing for gameplay - use controller.Pawn().
Every wrapper is a frame-local value around a raw entity pointer, and the engine frees entities between frames without telling anyone.
explicit operator bool() is the one validity check. There is no IsValid().A falsy wrapper reads zero and ignores writes. A stale non-null pointer does neither, which is why the rule is "never store one" rather than "check before use".
Schema fields are generated accessor pairs, not data members: pawn.Health() reads, pawn.SetHealth(100) writes and replicates. voltmod framework schemagen bakes the offsets in from a schema dump, and the load aborts when the live game no longer matches - see Gamedata and schema.
Fields on a pawn's services are reached the same way. The camera services pointer is typed as the base class, so view it as the CS subclass to reach the zoom fields:
A handle field reads as an EntityRef. Get the entity it points at:
An entity with no wrapper class is viewed through its generated class. A beam draws a line from its origin to its end point; set the fields before it spawns:
A Set on a networked field dirties it for the next snapshot. A field the engine does not network is written without a notify, because the engine rejects one and then stops updating that entity for its clients; a field with no route to notify generates no setter at all.
Adding a field means editing gamedata/schema/manifest.json and regenerating. An entry is m_name, m_name>Accessor to rename it, m_name:CppType to read it as that type, or m_name>Accessor:CppType; a class set to "*" takes every field the dump reports. Accessors keep the engine's name without its m_ and type prefix; rename one only where it clashes with a hand-written wrapper method. For a class with no curated wrapper, construct its generated view over the raw pointer:
Every accessor answers harmlessly on a falsy view. Use them only on the game thread.
runtime.Entities is both the factory and the lookup:
EyeAngles() is the pawn's networked aim and EyePosition() is the origin plus ViewOffset(), where shots originate. FlashDuration() and FlashMaxAlpha() carry what the last player_blind set, 255 max-alpha meaning a full blind; for blind-time bookkeeping prefer the typed PlayerBlind event, which carries the duration directly.
ShotsFired() counts the current burst and the engine resets it once the player stops firing. LastWeaponFireUsercmd() is the usercmd number of the last shot, which ties a weapon_fire event to its command. AimPunchServices() carries the recoil punch as the last shot set it, so read PredictableBaseAngle() and PredictableBaseTick() together - the engine decays the angle from that base each tick. EntitySpottedState() exposes the radar bits, and WeaponServices().ActiveWeapon() is the held weapon.
Vector and QAngle are the engine's own types; <VoltMod/Engine/Math.hpp> is the header to include for them, so a plugin never names an SDK path. AngleToForward, from the same header, turns an aim into the unit vector it points along, for tracing or placing something ahead of a player.
Observer mode is a method rather than a field: it lives on a sub-object the pawn points at, so there is no fixed offset to reach it.
PlayerName() is m_iszPlayerName, a 128-byte fixed buffer. SetPlayerName truncates to 127 characters plus NUL, and replication piggybacks on the next state-change broadcast, so pair a write with ChangeTeam or similar when the scoreboard has to refresh now.
VoltMod::Trace answers sight and reachability questions through the nav mesh's window onto the physics world. Nothing to install, nothing to re-take per map, and it survives map changes.
Line returns a VoltMod::TraceHit saying where the trace stopped, the surface normal there, HitEntity, the entity it hit, and HitWorld when that was the map itself; Clear is the yes/no form. FromEyes(pawn, distance) traces along a player's aim, ignoring the player. TraceOptions::Layers picks what stops it: Sight (world geometry and line-of-sight blockers, so windows and clips do not count) or Solid (what a player body collides with, other players included). IgnoreOwnedBy passes through everything an entity owns: spawn a multi-part prop with PropSpec::Owner set to its first part, and one trace ignores the whole prop.
Box sweeps a box instead of a line. A sweep that starts and ends at one point asks whether a box of that size fits there:
Box needs its own vtable slot, so it can be unsupported while Line works.
VoltMod::Rounds ends the current round through CCSGameRules::TerminateRound, so the win panel, round_end and the next round are the engine's own. It works with mp_ignore_round_win_conditions 1, which is how a mode with its own win rule runs:
Team scores are left alone.
It also sets the round timer. SetTime overrides mp_roundtime for the running round only, and the timer on screen follows at once, so a mode with its own round length sets it on every round_start. TimeLeft counts down to 0 and below; with win conditions off, the engine ends nothing when it runs out:
VoltMod::Team (<VoltMod/Engine/Team.hpp>, no SDK) is what Team() returns and ChangeTeam takes. The team lives on the controller:
The engine refuses a weapon the pawn's team cannot buy, so GiveItem retries with the pawn on the other team for the same frame and swaps it back before returning. It returns false when the engine refused the item both times or the item services did not bind.
runtime.Entities creates entities; the verbs are on the VoltMod::Entity itself. Build spawn data with VoltMod::KeyValues. runtime.Entities.Available() says whether spawning bound.
Never delete an entity: use Remove or RemoveAfter. Fields written between Create and Spawn go out with the first snapshot.
Props, particles and beams have their own spawners, which know the engine's traps: a prop that blocks nothing needs collision turned off as well as solid 0, and a line is a beam, never an env_beam, which takes the server down. A particle line takes its far end by handle: every entity name stays in the engine's string pool until the map ends, so a name per shot leaks.
VoltMod::Precache queues custom resources for the next map's session manifest. The host hooks the game rules system's manifest event once and hands every plugin the manifest, so a plugin loaded mid-map, by volt reload too, adds its resources at the next map load.
Assets that are not part of the map must also reach clients, such as through a workshop addon, or they precache server-side and render nothing.