# 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.

```ts
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](https://s2script.com/docs/concepts/schema-fields).

CS2's pawn-owned experimental camera is [Custom camera](https://s2script.com/docs/concepts/camera) (`Pawn.getCustomCamera()`). The camera is borrowed: disable it when finished; do not delete the entity.

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` (pre mutates; [`OnTakeDamagePost`](https://s2script.com/docs/concepts/sdkhooks#ontakedamagepost) is read-only). Per-viewer visibility is `Transmit.setVisibleTo` or `SDKHookType.SetTransmit`. Per-entity virtuals (`Spawn`, `Think`, `Use`, `Weapon*`, `Reload`) are `SDKHook`, not `OnEntitySpawned` and not `onOutput` ([SDKHooks](https://s2script.com/docs/concepts/sdkhooks)). See the [entity module](https://s2script.com/docs/api/modules/entity) and [lifecycle](https://s2script.com/docs/concepts/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.

```ts
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
	});
}
```
