Entities
An entity reference is EntityRef = { index, id } — a raw pointer never crosses to JavaScript. index is the slot in the game entity system; id is a host-minted liveness id (the engine serial never leaves Rust).
Every field access re-validates the ref against the host’s liveness books, then slot-matches the entity identity. A stashed ref whose entity was destroyed reads null / false, never garbage. The framework mints every EntityRef; plugin code never constructs one.
Create, spawn, and destroy are named publics. The host always subscribes "*"; class-name filters happen in the handler. Filtered entity I/O is the free load-window onOutput (SourceMod HookEntityOutput) — there is no catch-all OnEntityOutput public.
import { onOutput } from '@s2script/sdk';
import type { EntityRef } from '@s2script/sdk';
import { Player } from '@s2script/cs2';
export function OnEntitySpawned(entity: EntityRef | null, className: string): void {
if (className !== 'weapon_ak47') return;
// entity: EntityRef | null
}
export function OnPluginStart(): void {
onOutput('trigger_multiple', 'OnStartTouch', (ev) => {
console.log('touch', ev.caller?.name);
});
for (const p of Player.allConnected()) {
const hp = p.pawn?.health; // number | null
}
} onOutput throws after settle (same load window as command). Keyed by (classname, output) at the native mux — use "*" for either side. The create / destroy siblings are OnEntityCreated and OnEntityDestroyed. Damage is the named public OnTakeDamage(info) — mutate DamageInfo in place.
CS2 Pawn / Player are EntityRef-backed with generated schema accessors (health, origin, …). Handle fields return live refs; writes call notifyStateChanged automatically.
Accessors exist for 367 entity classes, including embedded structs (collision, glow) and enum fields. An entity you create yourself starts bare — wrapEntity applies a class’s accessors to any ref. See Schema fields.
Create/spawn/teleport/remove and ray casts live on @s2script/sdk/entity. Entity lifecycle is named publics (OnEntityCreated / OnEntitySpawned / OnEntityDestroyed) — filter by className in the handler. Filtered entity I/O is the free onOutput (SourceMod HookEntityOutput). Damage is the named public OnTakeDamage(info) (mutate DamageInfo in place) and per-entity SDKHook. Per-viewer visibility is Transmit.setVisibleTo or SDKHookType.SetTransmit. Per-entity virtuals (Spawn, Think, Use) are SDKHook, not OnEntitySpawned and not onOutput (SDKHooks). See the entity module and lifecycle.
TriggerZone.create (CS2) builds a runtime trigger_multiple that fires OnStartTouch/OnEndTouch. Hook those outputs with onOutput from @s2script/sdk — there is no Entity.onOutput.
Engine calls, not field writes
Some effects have no working schema equivalent — writing the netvar looks like it succeeded and does nothing. These are engine calls on EntityRef (and the matching Pawn wrappers):
| Call | Why a field write fails |
|---|---|
setGravityScale(scale) | CBaseEntity::SetGravityScale early-returns when unchanged and maintains m_flActualGravityScale. Writing m_flGravityScale is a no-op. 1 is normal, 0 is weightless. |
applyAbsVelocityImpulse([x, y, z]) | Physics-aware add. Writing m_vecAbsVelocity skips the partition/physics update; teleport(null, null, velocity) sets velocity absolutely. |
stopSound(name) | CBaseEntity::StopSound. Same call as Sound.stop(name, { entity }). |
setBodyGroupByName(name, group) | m_bodyGroupChoices is a CUtlOrderedMap, not a writable scalar. group is 32-bit. |
setModelScale(scale) | Argument shape is confirmed; the function name is a catalogue attribution the body does not itself prove. Calling it is memory-safe — verify the effect before relying on it in a shipped plugin. |
Each returns false if the op is unavailable, the ref is stale, or the argument is not finite.
CS2 Pawn.maxSpeed is a getter (CCSPlayerPawn::GetPlayerMaxSpeed), not a field — there is no m_flMaxSpeed on the pawn. It is null (never 0) when unavailable: 0 is a legitimate speed for a frozen player.
import { command } from '@s2script/sdk';
import { Pawn } from '@s2script/cs2';
export function OnPluginStart(): void {
command('boost', (cmd) => {
const pawn = Pawn.forSlot(cmd.callerSlot);
if (!pawn) return;
pawn.setGravityScale(0.5);
pawn.applyAbsVelocityImpulse([0, 0, 400]);
const cap = pawn.maxSpeed; // number | null
});
}