# Coming from ModSharp

This page is for ModSharp module authors moving to s2script. Much will feel familiar: named game events with pre and post handlers, client lifecycle callbacks, commands that return a result, repeating timers, and typed APIs shared between modules. The big shifts are the language (TypeScript, not C#), the plugin shape (exported functions, not a class that implements `IModSharpModule`), and cleanup (the host tears down everything you registered, so there is no `Shutdown` bookkeeping).

## Concept map

| ModSharp                                                                  | s2script                                                                                                                                                 |
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Class implementing `IModSharpModule`                                      | A package that exports `OnPluginStart` — [Authoring](https://s2script.com/docs/authoring)                                                                                    |
| `Init()` / `PostInit()`                                                   | `OnPluginStart` — [Lifecycle](https://s2script.com/docs/concepts/lifecycle)                                                                                                  |
| `Shutdown()`                                                              | `OnPluginEnd` (best-effort; the host ledger does the real teardown) — [Lifecycle](https://s2script.com/docs/concepts/lifecycle#teardown)                                     |
| `OnAllModulesLoaded()`                                                    | `OnAllPluginsLoaded` — [Lifecycle](https://s2script.com/docs/concepts/lifecycle)                                                                                             |
| `hotReload` constructor argument                                          | `previous()` + `OnPluginState` — [Lifecycle](https://s2script.com/docs/concepts/lifecycle#hot-reload-handoff)                                                                |
| `ISharedSystem.GetClientManager()`, `GetEntityManager()`, …               | Plain named imports from `@s2script/sdk` and `@s2script/cs2` — [API overview](https://s2script.com/docs/api/overview)                                                        |
| `IConVarManager.CreateServerCommand`                                      | `command.server` — [Commands](https://s2script.com/docs/concepts/commands)                                                                                                   |
| `IClientManager.InstallCommandCallback`                                   | `command` — [Commands](https://s2script.com/docs/concepts/commands)                                                                                                          |
| `IClientManager.InstallCommandListener`                                   | `command.onClientCommand` — [commands module](https://s2script.com/docs/api/modules/commands)                                                                                |
| `ECommandAction.Stopped` / `Skipped`                                      | `HookResult.Handled` / `HookResult.Continue` — [Commands](https://s2script.com/docs/concepts/commands#return-values)                                                         |
| `IEventListener.FireGameEvent`                                            | `hook.on` — [Events](https://s2script.com/docs/concepts/events)                                                                                                              |
| `IEventListener.HookFireEvent` (`serverOnly = true`)                      | `hook.onPre` returning `HookResult.Handled` — [Events](https://s2script.com/docs/concepts/events#pre-and-post)                                                               |
| `IClientListener` (`OnClientConnected`, `OnClientPostAdminCheck`, …)      | Named publics (`OnClientConnected`, `OnClientPostAdminCheck`, …) — [Lifecycle](https://s2script.com/docs/concepts/lifecycle#named-publics)                                   |
| `IGameListener` (`OnResourcePrecache`, `OnGameActivate`, …)               | Named publics (`OnPrecache`, `OnMapStart`, `OnMapEnd`, `OnGameFrame`) — [Lifecycle](https://s2script.com/docs/concepts/lifecycle#map-changes)                                |
| `IModSharp.PushTimer` / `StopTimer`                                       | `after` / `every` / `delay` and `Timer.kill()` — [timers module](https://s2script.com/docs/api/modules/timers)                                                               |
| `IModSharp.InvokeFrameAction`                                             | `await nextFrame()` — [Async](https://s2script.com/docs/concepts/async#timers)                                                                                               |
| `IGameClient.GetPlayerController()` → `GetPlayerPawn()`                   | `Player.fromSlot(slot)?.pawn` or `Pawn.forSlot(slot)` — [cs2 module](https://s2script.com/docs/api/modules/cs2)                                                              |
| `IEntityManager.GetPlayerControllers()`                                   | `Player.all()` / `Player.allConnected()` — [cs2 module](https://s2script.com/docs/api/modules/cs2)                                                                           |
| `IBaseEntity.IsValid()`, `GetNetVar` / `SetNetVar`                        | Liveness-checked `EntityRef` and generated schema accessors — [Entities](https://s2script.com/docs/concepts/entities), [Schema fields](https://s2script.com/docs/concepts/schema-fields)         |
| `IGameClient.Print(HudPrintChannel.Chat, …)` / `IModSharp.PrintToChatAll` | `Chat.toSlot` / `Chat.toAll` / `client.chat` — [chat module](https://s2script.com/docs/api/modules/chat)                                                                     |
| `IConVarManager.CreateConVar` / `FindConVar`                              | `Server.registerCvar` / `Server.getCvar` / `Server.setCvar`, or typed plugin [config](https://s2script.com/docs/concepts/config) — [server module](https://s2script.com/docs/api/modules/server) |
| AdminManager module: `RegisterAdminCommand` with permission strings       | `command.admin(name, ADMFLAG.X, handler)` and `Admin` — [admin module](https://s2script.com/docs/api/modules/admin)                                                          |
| `RegisterSharpModuleInterface` (in `PostInit`)                            | `publish` (in `OnPluginStart`) — [Interfaces](https://s2script.com/docs/concepts/interfaces)                                                                                 |
| `GetRequiredSharpModuleInterface` / `GetOptionalSharpModuleInterface`     | `use` / `watchOptional` (or one-shot `tryUse`) — [Interfaces](https://s2script.com/docs/concepts/interfaces#optional)                                                        |
| `OnLibraryConnected` / `OnLibraryDisconnect`                              | The `watchOptional` attach callback and its `scope` — [Interfaces](https://s2script.com/docs/concepts/interfaces#optional)                                                   |
| `dotnet publish` to `sharp/modules/{AssemblyName}`                        | `s2s build` to a `.s2sp`, dropped into `addons/s2script/plugins/` — [Authoring](https://s2script.com/docs/authoring#scaffold)                                                |

## Side by side

### Module skeleton and lifecycle

A ModSharp module is one class with a fixed constructor signature. It registers in `Init`, publishes shared interfaces in `PostInit`, and undoes its registrations in `Shutdown`.

```csharp
public sealed class Example : IModSharpModule
{
    private readonly ISharedSystem _sharedSystem;

    public Example(ISharedSystem sharedSystem, string dllPath, string sharpPath,
                   Version version, IConfiguration configuration, bool hotReload)
        => _sharedSystem = sharedSystem;

    public bool Init()
    {
        Console.WriteLine("Hello, World!");
        return true;
    }

    public void PostInit() { }
    public void OnAllModulesLoaded() { }

    public void Shutdown() => Console.WriteLine("Byebye, World!");

    public string DisplayName   => "Example";
    public string DisplayAuthor => "YourName";
}
```

An s2script plugin exports functions. There is no constructor and no service locator. The plugin's name and version come from `package.json`.

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

let reloads = 0;

export function OnPluginStart(): void {
	// Init + PostInit: register commands, hooks, and interfaces here.
	const prev = previous() as { reloads: number } | undefined;
	reloads = (prev?.reloads ?? 0) + 1;
	console.log('Hello, World!');
}

export function OnAllPluginsLoaded(): void {
	// The plugin set is stable. This is not a second registration window.
}

export function OnPluginState(): unknown {
	return { reloads }; // handed to the next instance as previous() on hot reload
}

export function OnPluginEnd(): void {
	console.log('Byebye, World!');
}
```

### A command

ModSharp splits commands across two managers, and you release each one in `Shutdown`.

```csharp
public bool Init()
{
    _sharedSystem.GetConVarManager()
                 .CreateServerCommand("ms_echo", OnServerCommand, "Echo", ConVarFlags.Release);
    _sharedSystem.GetClientManager().InstallCommandCallback("hello", OnClientCommand);
    return true;
}

public void Shutdown()
{
    _sharedSystem.GetConVarManager().ReleaseServerCommandCallback("ms_echo", OnServerCommand);
    _sharedSystem.GetClientManager().RemoveCommandCallback("hello", OnClientCommand);
}

private ECommandAction OnServerCommand(StringCommand command)
{
    Console.WriteLine("Hello");
    return ECommandAction.Stopped;
}

private ECommandAction OnClientCommand(IGameClient client, StringCommand command)
{
    var name = command.ArgCount > 0 ? command.GetArg(1) : "YunLi";
    client.ConsolePrint($"Hello, {name}");
    return ECommandAction.Stopped;
}
```

In s2script, `command` covers players and the console, and `command.server` is server-console only. Chat `!` / `/` triggers reach the same registry. There is no release step.

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

export function OnPluginStart(): void {
	command.server('ms_echo', () => {
		console.log('Hello');
		return HookResult.Handled;
	});

	command('hello', (cmd) => {
		const name = cmd.argCount > 0 ? cmd.arg(0) : 'YunLi'; // arg() is 0-based
		cmd.replyToConsole(`Hello, ${name}`);
		return HookResult.Handled;
	});
}
```

> [!NOTE]
> `InstallCommandCallback("hello", …)` registers `ms_hello`. `command('hello', …)` registers `hello` exactly as written. Pick the full console name you want.

### An event hook

ModSharp delivers every event to one listener object. `HookFireEvent` runs before the event fires. Return `false` to block it, or set `serverOnly` to keep it from clients. `FireGameEvent` is the read-only listener.

```csharp
public bool Init()
{
    _sharedSystem.GetEventManager().InstallEventListener(this);
    return true;
}

public void Shutdown() => _sharedSystem.GetEventManager().RemoveEventListener(this);

public void FireGameEvent(IGameEvent e)
{
    if (e.Name.Equals("player_spawn"))
        Console.WriteLine($"Player slot[{e.GetInt("userid")}] spawned");
}

public bool HookFireEvent(IGameEvent e, ref bool serverOnly)
{
    if (e.Name.Equals("player_changename", StringComparison.OrdinalIgnoreCase))
        serverOnly = true; // silence for clients
    return true;
}

int IEventListener.ListenerVersion  => IEventListener.ApiVersion;
int IEventListener.ListenerPriority => 0;
```

s2script subscribes per event name. `hook.on` is post, and `hook.onPre` is pre. Returning `HookResult.Handled` from `onPre` is the `serverOnly = true` case: clients do not receive the event, and the server still processes it.

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

export function OnPluginStart(): void {
	hook.on('player_spawn', (ev) => {
		console.log(`Player slot[${ev.getPlayerSlot('userid')}] spawned`);
	});

	hook.onPre('player_changename', () => {
		return HookResult.Handled; // silence for clients
	});
}
```

To change fields before broadcast, call a setter such as `ev.setString('weapon', 'knife')` in `onPre` and return `HookResult.Changed`. See [Events](https://s2script.com/docs/concepts/events#return-values).

### A timer

ModSharp timers take seconds and return a `Guid`. A `Func<TimerAction>` callback can return `TimerAction.Stop` to end a repeating timer.

```csharp
var ticks = 0;
var id = _sharedSystem.GetModSharp().PushTimer(() =>
{
    Console.WriteLine("tick");
    return ++ticks >= 5 ? TimerAction.Stop : TimerAction.Continue;
}, 1.0, GameTimerFlags.Repeatable);

// elsewhere
_sharedSystem.GetModSharp().StopTimer(id);
```

s2script timers take **milliseconds**. `after` runs once, `every` repeats, and both return a `Timer` you can `kill()`, including from inside its own callback. `delay` is the `await` form.

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

let ticks = 0;
const timer = every(1000, () => {
	console.log('tick');
	if (++ticks >= 5) timer.kill();
});

async function later(): Promise<void> {
	await delay(2500);
	console.log('2.5s later');
}
```

### Player and pawn access

In ModSharp you go from the client to the controller to the pawn.

```csharp
private void OnCommandKill(IGameClient? issuer, StringCommand cmd)
{
    if (issuer?.GetPlayerController()?.GetPlayerPawn() is not { } pawn)
        return;

    if (!pawn.IsAlive)
    {
        pawn.Print(HudPrintChannel.Chat, "You are dead, you can't kill yourself.");
        return;
    }

    pawn.Slay();
}
```

In s2script, `Player` is the controller and `Pawn` is the body. Both are looked up by slot and return `null` when absent or stale.

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

export function OnPluginStart(): void {
	command('kill', (cmd) => {
		const pawn = Player.fromSlot(cmd.callerSlot)?.pawn; // or Pawn.forSlot(cmd.callerSlot)
		if (!pawn) {
			Chat.toSlot(cmd.callerSlot, "You are dead, you can't kill yourself.");
			return HookResult.Handled;
		}
		pawn.slay();
		return HookResult.Handled;
	});
}
```

`Player.pawn` is `null` when the player is dead or has no body. Go back from a pawn with `pawn.controller`.

### Sharing an API between modules

ModSharp shares a C# interface through a separate `.Shared` assembly. The provider registers it in `PostInit`. The consumer caches an `IModSharpModuleInterface<T>` and re-resolves it when the provider reconnects.

```csharp
// SharedInterface.Shared
public interface IMySharedModule
{
    const string Identity = nameof(IMySharedModule);
    void CallMe();
}

// Provider
public void PostInit()
    => _modules.RegisterSharpModuleInterface<IMySharedModule>(this, IMySharedModule.Identity, this);

// Consumer
private IModSharpModuleInterface<IMySharedModule>? _cached;

public void OnAllModulesLoaded()
{
    _cached = _modules.GetOptionalSharpModuleInterface<IMySharedModule>(IMySharedModule.Identity);
    _cached?.Instance?.CallMe();
}
```

In s2script the identity is the provider's npm package name. The contract is a `.d.ts` file named by the package `types` field. Set `interfaceProtocol` to `2` on both provider and consumer.

```json
{
	"name": "@demo/counter",
	"version": "1.0.0",
	"types": "api.d.ts",
	"main": "src/plugin.ts",
	"s2script": { "interfaceProtocol": 2, "publishes": "self" }
}
```

```ts
// api.d.ts
export interface Contract {
	methods: { getCount(): number };
	forwards: {};
}
```

```ts
// provider: src/plugin.ts
import { publish } from '@s2script/sdk/plugin';

export function OnPluginStart(): void {
	publish('@demo/counter', { getCount: () => 42 });
}
```

A hard consumer lists the provider under `s2script.pluginDependencies` and calls `use`. An optional consumer lists it under `s2script.optionalPluginDependencies` and calls `watchOptional`, which does the job of `OnLibraryConnected` / `OnLibraryDisconnect`.

```ts
// consumer: src/plugin.ts
import { watchOptional } from '@s2script/sdk/plugin';

export function OnPluginStart(): void {
	watchOptional('@demo/counter', (counter) => {
		console.log(counter.getCount()); // runs again after each provider reload
	});
}
```

The consumer's types come from the provider's declaration: `s2s build` writes them to `.s2script/interfaces.d.ts`, which your `tsconfig.json` must include. Values cross between plugins by copy and are checked against the contract. See [Wire values](https://s2script.com/docs/concepts/interfaces#wire).

## What's different

- **No cleanup code.** ModSharp docs require you to release commands, listeners, and hooks in `Shutdown`. The s2script host ledger owns every command, event subscription, timer, and imported interface, and removes them on unload. `OnPluginEnd` is for best-effort work only.
- **Registration has a window.** `command`, `hook.on` / `hook.onPre`, `publish`, `use`, `watchOptional`, and `previous()` work only during `OnPluginStart`. They throw after the plugin settles. Module top-level code runs before that window, so do not register there.
- **No dependency injection.** There is no `ISharedSystem` and no `ServiceCollection`. Import what you need. Stateless helpers such as `Chat`, `Admin`, `config`, and `Translations` work anywhere.
- **Units change.** Timers take milliseconds, not seconds. Command arguments are 0-based (`cmd.arg(0)`), where ModSharp's first argument is `GetArg(1)`.
- **One result type.** Commands, `onPre` handlers, and say hooks all return `HookResult` (`Continue`, `Changed`, `Handled`, `Stop`). Returning nothing means `Continue`.
- **Admin is flag-based.** s2script uses SourceMod-style bit flags (`ADMFLAG.KICK`, `ADMFLAG.SLAY`, …) and `Admin.forSlot(slot)?.hasFlags(…)`, not permission strings such as `admin_offensive:slay`.
- **Handles follow a connection.** A `Client` or `Player` handle belongs to one connection lifetime. After a reconnect, the same slot is a different handle. Re-resolve by slot or `userId` instead of keeping old handles.
- **Client publics fire only for new connections.** They fire for clients that connect after your plugin is active. To cover players already on the server, loop over `Clients.all()` in `OnPluginStart`.
- **Plugins survive map changes.** `OnPluginStart` does not re-run per map. Use `OnMapStart` for map-aware work.

> [!NOTE]
> In ModSharp, `HookFireEvent` can return `false` to stop an event firing at all. In s2script, `Handled` and `Stop` hide the event from clients, but the server still processes it. To prevent damage, use `OnTakeDamage` via [SDKHooks](https://s2script.com/docs/concepts/sdkhooks).

> [!WARNING]
> A `GameEvent` is valid only while your handler runs synchronously. Read what you need before any `await`.

> [!NOTE]
> Hot reload is a file replace. Rebuild and overwrite the `.s2sp` in `addons/s2script/plugins/`. There is no `reload` folder and no map change. Carry state across the reload with `OnPluginState` and `previous()`.

## Next

- [Getting started](https://s2script.com/docs/getting-started): install the runtime
- [Authoring plugins](https://s2script.com/docs/authoring): scaffold, `package.json`, the typecheck gate
- [Plugin lifecycle](https://s2script.com/docs/concepts/lifecycle): every named public, hot reload, map changes
- [Inter-plugin interfaces](https://s2script.com/docs/concepts/interfaces): protocol 2, forwards, `watchOptional`
- [API overview](https://s2script.com/docs/api/overview) and the [modules catalog](https://s2script.com/docs/api/modules)
