# Coming from SourceMod

s2script's APIs follow SourceMod's shape on purpose. The forwards, admin flags, `HookResult` values and SDKHook types keep their SourceMod names, so most of what you know carries over. The main changes: you write TypeScript instead of SourcePawn, and you get typed objects instead of handles. Forwards become named exported functions. You build a `.s2sp` archive instead of compiling a `.smx`, and a replaced archive hot-reloads.

## Concept map

| SourceMod                                                                       | s2script                                                                                                                  |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `.sp` → `spcomp` → `.smx`                                                       | npm package → `s2s build` → `.s2sp` ([Authoring](https://s2script.com/docs/authoring))                                                        |
| `addons/sourcemod/plugins/`, `sm plugins reload`                                | `addons/s2script/plugins/`: drop to load, replace to hot-reload, delete to unload ([Authoring](https://s2script.com/docs/authoring#scaffold)) |
| `public Plugin myinfo`                                                          | `package.json` `name` / `version` ([Authoring](https://s2script.com/docs/authoring#packagejson))                                              |
| `OnPluginStart` / `OnPluginEnd` / `OnMapStart` / `OnMapEnd`                     | Exported functions with the same names ([Lifecycle](https://s2script.com/docs/concepts/lifecycle))                                            |
| `OnClientPutInServer` / `OnClientPostAdminCheck` / `OnClientDisconnect`         | Same names; they receive a `Client` handle, not an index ([Lifecycle](https://s2script.com/docs/concepts/lifecycle#named-publics))            |
| `OnGameFrame`, `OnEntityCreated`, `OnPlayerRunCmd`                              | Same-name exported functions ([Lifecycle](https://s2script.com/docs/concepts/lifecycle))                                                      |
| `RegConsoleCmd` / `RegAdminCmd` / `RegServerCmd`                                | `command` / `command.admin` / `command.server` ([Commands](https://s2script.com/docs/concepts/commands))                                      |
| `AddCommandListener`                                                            | `command.onClientCommand` ([commands module](https://s2script.com/docs/api/modules/commands))                                                 |
| `GetCmdArgs` / `GetCmdArg` / `GetCmdArgString`                                  | `cmd.argCount` / `cmd.arg(n)` (0-based) / `cmd.argString`                                                                 |
| `ReplyToCommand`                                                                | `cmd.reply` ([commands module](https://s2script.com/docs/api/modules/commands))                                                               |
| `Plugin_Continue` / `Plugin_Changed` / `Plugin_Handled` / `Plugin_Stop`         | `HookResult.Continue` / `Changed` / `Handled` / `Stop` ([events module](https://s2script.com/docs/api/modules/events))                        |
| `ADMFLAG_KICK`, `ADMFLAG_SLAY`, …                                               | `ADMFLAG.KICK`, `ADMFLAG.SLAY`, … (same bit values) ([admin module](https://s2script.com/docs/api/modules/admin))                             |
| `CheckCommandAccess`, `CanUserTarget`                                           | `Admin.forSlot(slot)?.hasFlags(…)`, `Admin.canTarget` ([admin module](https://s2script.com/docs/api/modules/admin))                           |
| `ProcessTargetString` / `FindTarget`                                            | `Player.target(pattern, callerSlot)` ([cs2 module](https://s2script.com/docs/api/modules/cs2))                                                |
| `GetClientOfUserId`                                                             | `Player.fromUserId`, or `ev.getPlayerSlot('userid')` inside an event                                                      |
| `HookEvent` (post / `EventHookMode_Pre`)                                        | `hook.on` / `hook.onPre` ([Events](https://s2script.com/docs/concepts/events))                                                                |
| `FireEvent`, `FireToClient`                                                     | `Events.fire`, `Events.fireToClient` ([events module](https://s2script.com/docs/api/modules/events))                                          |
| `HookEntityOutput`                                                              | `onOutput` ([Entities](https://s2script.com/docs/concepts/entities))                                                                          |
| `CreateTimer` / `TIMER_REPEAT`                                                  | `after` / `every`, or `await delay(ms)` ([Async](https://s2script.com/docs/concepts/async#timers))                                            |
| `GetEntProp` / `SetEntProp` (`m_iHealth`, …)                                    | Generated schema accessors: `pawn.health`, `wrapEntity(…)` ([Schema fields](https://s2script.com/docs/concepts/schema-fields))                |
| `CreateEntityByName` / `DispatchSpawn` / `TeleportEntity` / `AcceptEntityInput` | `createEntity` / `ref.spawn()` / `ref.teleport` / `ref.acceptInput` ([entity module](https://s2script.com/docs/api/modules/entity))           |
| `SDKHook(client, SDKHook_OnTakeDamage, cb)`                                     | `SDKHook(entity, SDKHookType.OnTakeDamage, cb)` ([SDKHooks](https://s2script.com/docs/concepts/sdkhooks))                                     |
| `CreateConVar` / `HookConVarChange` / `SetConVarString`                         | `Server.registerCvar` / `Server.onCvarChange` / `Server.setCvar` ([server module](https://s2script.com/docs/api/modules/server))              |
| `AutoExecConfig` + `cfg/sourcemod/*.cfg`                                        | `s2script.config` in `package.json` + `configs/<plugin-id>.json` ([Config](https://s2script.com/docs/concepts/config))                        |
| `PrintToChat` / `PrintToChatAll`                                                | `Chat.toSlot` or `client.chat` / `Chat.toAll` ([chat module](https://s2script.com/docs/api/modules/chat))                                     |
| `LoadTranslations` / `%t` / `%T`                                                | `translations.load` / `cmd.replyT` / `Translations.translate` ([Translations](https://s2script.com/docs/concepts/translations))               |
| `CreateNative` / `CreateGlobalForward`                                          | `publish` with contract `methods` / `forwards` ([Interfaces](https://s2script.com/docs/concepts/interfaces))                                  |
| `OnLibraryAdded` / `OnLibraryRemoved`                                           | `watchOptional` ([Interfaces](https://s2script.com/docs/concepts/interfaces#optional))                                                        |
| `RegClientCookie` / `GetClientCookie` / `SetClientCookie`                       | `Cookies.register` / `Cookies.get` / `Cookies.set` ([Cookies](https://s2script.com/docs/concepts/cookies))                                    |
| `SQL_TConnect` / `Database.Connect`                                             | `await Database.open(name)` ([Async](https://s2script.com/docs/concepts/async#database))                                                      |
| `Menu` / `VoteMenu`                                                             | `Menu` / `Vote.start` ([menu module](https://s2script.com/docs/api/modules/menu), [votes module](https://s2script.com/docs/api/modules/votes))                    |
| `KeyValues`                                                                     | No equivalent. Use JSON with `config.readFile` / `config.writeFile` ([Config](https://s2script.com/docs/concepts/config#raw-files))           |
| basecommands, basechat, basebans, adminmenu, …                                  | Ship in the release zip as `.s2sp` plugins ([Getting started](https://s2script.com/docs/getting-started#base-plugins), [catalog](https://s2script.com/plugins))   |

The release includes the SourceMod base suite: basecommands, basechat, playercommands, antiflood, adminhelp, basecomm, basebans, reservedslots, basetriggers, funcommands, clientprefs, adminmenu and basevotes, plus zones. nominations, rockthevote, nextmap and funvotes are opt-in and ship under `plugins/disabled/`.

## Side by side

### Plugin skeleton

```c
#include <sourcemod>

public Plugin myinfo = {
    name = "Hello",
    author = "me",
    description = "Says hello",
    version = "1.0.0",
    url = ""
};

public void OnPluginStart()
{
    RegConsoleCmd("sm_hello", Command_Hello);
}

public Action Command_Hello(int client, int args)
{
    ReplyToCommand(client, "Hello!");
    return Plugin_Handled;
}
```

```ts
import { command, HookResult } from '@s2script/sdk';

export function OnPluginStart(): void {
	command('sm_hello', (cmd) => {
		cmd.reply('Hello!');
		return HookResult.Handled;
	});
}
```

Plugin metadata goes in `package.json`. The host looks up `OnPluginStart` and the other forwards by name on your module's exports. You don't need a `public` keyword or a callback name string. Register commands, hooks and translations inside `OnPluginStart`. Registering them at module top level throws.

### Admin command with arguments and entity props

```c
public void OnPluginStart()
{
    RegAdminCmd("sm_addhp", Command_AddHp, ADMFLAG_SLAY, "sm_addhp <#userid|name> <amount>");
}

public Action Command_AddHp(int client, int args)
{
    if (args < 2)
    {
        ReplyToCommand(client, "Usage: sm_addhp <#userid|name> <amount>");
        return Plugin_Handled;
    }

    char pattern[64];
    GetCmdArg(1, pattern, sizeof(pattern));
    int amount = GetCmdArgInt(2);

    int target = FindTarget(client, pattern);
    if (target == -1)
        return Plugin_Handled;

    int hp = GetEntProp(target, Prop_Send, "m_iHealth");
    SetEntProp(target, Prop_Send, "m_iHealth", hp + amount);
    ReplyToCommand(client, "%N now has %d HP.", target, hp + amount);
    return Plugin_Handled;
}
```

```ts
import { command, ADMFLAG, HookResult } from '@s2script/sdk';
import { Player } from '@s2script/cs2';

export function OnPluginStart(): void {
	command.admin('sm_addhp', ADMFLAG.SLAY, (cmd) => {
		if (cmd.argCount < 2) {
			cmd.reply('Usage: sm_addhp <#userid|name> <amount>');
			return HookResult.Handled;
		}

		const targets = Player.target(cmd.arg(0), cmd.callerSlot, true);
		const amount = cmd.argInt(1);
		if (targets.length === 0) {
			cmd.reply('No matching player.');
			return HookResult.Handled;
		}

		for (const player of targets) {
			const pawn = player.pawn;
			const hp = pawn?.health;
			if (pawn && hp != null) pawn.health = hp + amount;
		}
		cmd.reply(`Added ${amount} HP to ${targets.length} player(s).`);
		return HookResult.Handled;
	});
}
```

Arguments are 0-based. `GetCmdArg(1)` becomes `cmd.arg(0)`. `cmd.argInt` and `cmd.argFloat` replace the buffer-and-convert step.

In CS2, health lives on the pawn (the in-world body), not the controller. Fields are generated accessors, so a typo is a compile error instead of a runtime prop lookup failure. Reads return `null` on a stale entity, and writes send the network update for you. For entities without a wrapper, use `wrapEntity('CBaseModelEntity', ref)`. See [Schema fields](https://s2script.com/docs/concepts/schema-fields).

### Event hooks, post and pre

```c
public void OnPluginStart()
{
    HookEvent("player_death", Event_PlayerDeath);
    HookEvent("player_team", Event_PlayerTeam, EventHookMode_Pre);
}

public void Event_PlayerDeath(Event event, const char[] name, bool dontBroadcast)
{
    int attacker = GetClientOfUserId(event.GetInt("attacker"));
    if (attacker > 0)
        PrintToChat(attacker, "Nice shot.");
}

public Action Event_PlayerTeam(Event event, const char[] name, bool dontBroadcast)
{
    return Plugin_Handled;
}
```

```ts
import { hook, Chat, HookResult } from '@s2script/sdk';

export function OnPluginStart(): void {
	hook.on('player_death', (ev) => {
		const attacker = ev.getPlayerSlot('attacker');
		if (attacker >= 0) Chat.toSlot(attacker, 'Nice shot.');
	});

	hook.onPre('player_team', () => HookResult.Handled);
}
```

> [!IMPORTANT]
> In a pre hook, `HookResult.Handled` and `Stop` suppress the **client broadcast**. The server still processes the event. This is closer to setting `event.BroadcastDisabled` than to SourceMod's full block. `Stop` also skips lower-priority handlers.

The `GameEvent` is valid only during the synchronous handler. Read the fields you need before any `await`. For typed field names, use `Events.on` from `@s2script/cs2`.

### Repeating timer

```c
int g_iShown;

public void OnPluginStart()
{
    CreateTimer(60.0, Timer_Advert, _, TIMER_REPEAT);
}

public Action Timer_Advert(Handle timer)
{
    PrintToChatAll("Visit example.com");
    if (++g_iShown >= 10)
        return Plugin_Stop;
    return Plugin_Continue;
}
```

```ts
import { Chat } from '@s2script/sdk';
import { every } from '@s2script/sdk/timers';

export function OnPluginStart(): void {
	let shown = 0;
	const advert = every(60_000, () => {
		Chat.toAll('Visit example.com');
		if (++shown >= 10) advert.kill();
	});
}
```

Intervals are in **milliseconds**, not seconds. Stop a timer with `timer.kill()` instead of returning `Plugin_Stop`. `after(ms, fn)` is the one-shot form. In async code, `await delay(ms)` is often simpler. Timers belong to your plugin and are killed on unload, so there's no handle to close. There is no `TIMER_FLAG_NO_MAPCHANGE`. If a timer shouldn't outlive the map, kill it in `OnMapEnd`.

### ConVars and config

```c
ConVar g_cvEnabled;

public void OnPluginStart()
{
    g_cvEnabled = CreateConVar("sm_greet_enabled", "1", "Greet players", _, true, 0.0, true, 1.0);
    AutoExecConfig(true, "greet");
}

public void OnClientPostAdminCheck(int client)
{
    if (g_cvEnabled.BoolValue)
        PrintToChat(client, "Welcome!");
}
```

Most plugin settings belong in typed config. Declare them in `package.json`:

```json
{
	"s2script": {
		"config": {
			"enabled": { "type": "bool", "default": true, "description": "Greet players" }
		}
	}
}
```

```ts
import { config } from '@s2script/sdk';
import type { Client } from '@s2script/sdk';

export function OnClientPostAdminCheck(client: Client): void {
	if (config.getBool('enabled')) client.chat('Welcome!');
}
```

On first load, the host writes `addons/s2script/configs/<plugin-id>.json` with the defaults, which works like `AutoExecConfig`. Operators edit that file. Subscribe with `config.onChange` to pick up edits without a reload.

If operators need a real console variable, register one:

```ts
import { Server } from '@s2script/sdk/server';

export function OnPluginStart(): void {
	Server.registerCvar('sm_greet_enabled', {
		type: 'int',
		default: 1,
		min: 0,
		max: 1,
		help: 'Greet players'
	});
	Server.onCvarChange('sm_greet_enabled', (_name, next) => console.log('now', next));
}

function greetEnabled(): boolean {
	return Number(Server.getCvar('sm_greet_enabled')) !== 0;
}
```

`Server.getCvar` returns a string. As in SourceMod, a registered cvar and its value persist across plugin reloads.

### Translations

```c
// translations/greet.phrases.txt
"Phrases"
{
    "Welcome"
    {
        "#format"   "{1:s}"
        "en"        "Welcome, {1}!"
    }
}
```

```c
public void OnPluginStart()
{
    LoadTranslations("greet.phrases");
    RegConsoleCmd("sm_welcome", Command_Welcome);
}

public Action Command_Welcome(int client, int args)
{
    ReplyToCommand(client, "%t", "Welcome", "friend");
    return Plugin_Handled;
}
```

```json
{
	"Welcome": "{green}Welcome, {1}!"
}
```

```ts
import { command, translations, Translations, Chat, HookResult } from '@s2script/sdk';

export function OnPluginStart(): void {
	translations.load('greet');

	command('sm_welcome', (cmd) => {
		cmd.replyT('Welcome', 'friend');
		return HookResult.Handled;
	});
}

function greet(slot: number, name: string): void {
	Chat.toSlot(slot, Translations.translate(slot, 'Welcome', name)); // SourceMod's %T
}
```

Phrase files are flat JSON at `translations/<set>.phrases.json`. Other languages go in `translations/<code>/<set>.phrases.json`. `{1}`, `{2}` are positional slots, with no `#format` needed. Colour tags such as `{green}` expand on output. `s2s build` checks every key against the files you load, so a misspelled key is a compile error.

## What's different

- **Slots, not client indexes.** A player is a 0-based slot, and the server console is `-1`. SourceMod client `1` is slot `0`. Where you'd guard `client == 0` for the console, check `cmd.callerSlot < 0`.
- **Handles are connection lifetimes.** A `Client` or `Player` handle goes stale when that connection leaves. It never points at the next person in the same slot. Stale reads return defaults (`"0"`, `""`, `null`), not garbage. `isValid()` replaces `IsClientInGame` checks on a saved handle. See [client handles](https://s2script.com/docs/concepts/lifecycle#client-handles).
- **No `CloseHandle`.** The host tracks every command, hook, timer, database and socket your plugin owns, and tears them down on unload. `OnPluginEnd` is for best-effort work only.
- **Forwards are exports.** A forward you don't export is never subscribed. Game events are the one exception: they go through `hook.on` / `hook.onPre`, not exported functions.
- **Client forwards don't replay.** As in SourceMod, `OnClientPutInServer` only fires for clients who connect after load. Seed already-connected players in `OnPluginStart` with `Clients.all()`.
- **Async instead of callbacks.** Database, HTTP and sockets return Promises. `await` replaces the `SQL_TQuery` callback-plus-`data` pattern. See [Async](https://s2script.com/docs/concepts/async).
- **Hot reload keeps state if you ask.** Return state from `OnPluginState`, then read it back with `previous()` in the new instance's `OnPluginStart`. See [Hot reload handoff](https://s2script.com/docs/concepts/lifecycle#hot-reload-handoff).
- **`OnPluginStart` runs once per load, not per map.** Put per-map work in `OnMapStart`. `OnMapEnd` is derived from the next map start, because CS2 has no level-shutdown callback.
- **Natives are typed contracts.** A producer `publish`es methods and forwards declared in a `.d.ts`. Consumers `import` them directly or call `use`. Values cross by copy and are validated. See [Interfaces](https://s2script.com/docs/concepts/interfaces).
- **Strict typecheck at build.** `s2s build` refuses to emit a `.s2sp` on any type error. A failed rebuild leaves the running plugin untouched.

> [!NOTE]
> Returning nothing from a command or hook is the same as `HookResult.Continue`. For a command you own, return `HookResult.Handled` on every path, including usage errors.

> [!WARNING]
> Don't copy SourceMod or CounterStrikeSharp vtable offsets or signatures into s2script gamedata. Resolve engine facts against your own CS2 binary. See [Engine calls](https://s2script.com/docs/concepts/engine-calls).

## Next

- [Getting started](https://s2script.com/docs/getting-started): install the runtime and base plugins
- [Authoring plugins](https://s2script.com/docs/authoring): scaffold, build, plugin shape
- [Plugin lifecycle](https://s2script.com/docs/concepts/lifecycle): every named forward
- [Commands](https://s2script.com/docs/concepts/commands) and [Events](https://s2script.com/docs/concepts/events)
- [SDKHooks](https://s2script.com/docs/concepts/sdkhooks) and [Entities](https://s2script.com/docs/concepts/entities)
- [Inter-plugin interfaces](https://s2script.com/docs/concepts/interfaces): natives and forwards
- [API overview](https://s2script.com/docs/api/overview) and [modules catalog](https://s2script.com/docs/api/modules)
