swiftlys2/docs/api/ai/

Using Attributes

SwiftlyS2 lets you register commands, events, and hooks by decorating methods with attributes, instead of wiring every handler manually.

Registration Scope

Attribute discovery runs on registered object instances:

  • Your plugin class (the one inheriting BasePlugin) is registered automatically.
  • Any other class must be registered explicitly with Core.Registrator.Register(instance).
public sealed class ModerationHandlers
{
    [ClientChatHookHandler]
    public HookResult OnClientChat(int playerId, string text, bool teamOnly)
    {
        if (text.Contains("badword", StringComparison.OrdinalIgnoreCase))
        {
            return HookResult.Stop;
        }
 
        return HookResult.Continue;
    }
}
 
public override void Load(bool hotReload)
{
    Core.Registrator.Register(new ModerationHandlers());
}

Register each handler instance once - registering the same instance repeatedly duplicates callbacks.

Signature Rules

Attribute-decorated methods must match the expected delegate: same parameter order/types, same return type (void vs HookResult), and same generic event/message type where the attribute is generic. Otherwise the handler won't be picked up.

Attribute Reference

APIPluginMetadata

On your plugin class. Id and Version are required; the rest optional:

[PluginMetadata(Id = "my.plugin", Version = "1.0.0", Name = "My Plugin", Author = "Author", Description = "...", Website = "...")]
public partial class MyPlugin : BasePlugin
{
}

APICommand

Registers a chat/console command; permission gates it via Permissions. Full registration/lookup API on APIICommandService - see Commands.

[Command("heal", permission: "myplugin.heal", helpText: "Heals yourself")]
public void OnHealCommand(ICommandContext context)
{
    var player = context.Sender;
}

APICommandAlias

Adds an alternate name for a command registered elsewhere (attribute or APIICommandService.RegisterCommand):

[CommandAlias("heal", "h")]
private void HealAlias() { } // body is unused, only the attribute matters

APIGameEventHandler

Hooks a generated game event (EventPlayerDeath, EventRoundStart, ...) - see APIIGameEventService and Game Events for the full hook/fire API. HookMode.Pre runs before the game processes it, Post runs after:

[GameEventHandler(HookMode.Post)]
private HookResult OnPlayerDeath(EventPlayerDeath @event)
{
    return HookResult.Continue;
}

APIEventListener`1 ([EventListener<T>])

Subscribes a method to one of the framework's own events, via delegates on APIEventDelegates mirroring APIIEventSubscriber (e.g. EventDelegates.OnEntityCreated):

[EventListener<EventDelegates.OnEntityCreated>]
public void OnEntityCreated(IOnEntityCreatedEvent @event)
{
}

This is the attribute-based equivalent of Core.Event.OnEntityCreated += OnEntityCreated;.

APIGameHookHandler

Hooks a category under APIIGameHooks (e.g. entity take-damage):

[GameHookHandler(HookMode.Pre)]
private void OnTakeDamagePre(ref TakeDamageEntityPreContext ctx)
{
}

APIClientCommandHookHandler

Intercepts a raw client console command before the server processes it - see APIICommandService.HookClientCommand:

[ClientCommandHookHandler]
public HookResult OnClientCommand(int playerId, string commandLine)
{
    return HookResult.Continue;
}

APIClientChatHookHandler

Intercepts a chat message before it's broadcast - same shape as above, with chat-specific parameters, see APIICommandService.HookClientChat (and the ModerationHandlers example at the top of this page):

[ClientChatHookHandler]
public HookResult OnClientChat(int playerId, string text, bool teamOnly)
{
    return HookResult.Continue;
}

APIEntityOutputHandlerAttribute / APIEntityOutputHandlerAttribute`1

Hooks an entity I/O output by designer name, or generically by schema class (<T> resolves T.ClassName for you):

[EntityOutputHandler("func_door", "OnOpen")]
public void OnDoorOpened(IOnEntityFireOutputHookEvent @event)
{
    Console.WriteLine($"Output '{@event.OutputName}' fired by '{@event.DesignerName}'");
}
 
[EntityOutputHandler<CBaseDoor>("OnOpen")]
public void OnDoorOpenedGeneric(IOnEntityFireOutputHookEvent @event)
{
}

Set @event.Result = HookResult.Stop to block the output from firing.

APIEntityInputHandlerAttribute / APIEntityInputHandlerAttribute`1

Same shape as the output handler, for entity I/O inputs instead:

[EntityInputHandler("func_door", "Open")]
public void OnDoorOpenInput(IOnEntityIdentityAcceptInputHookEvent @event)
{
    Console.WriteLine($"Input '{@event.InputName}' accepted by '{@event.DesignerName}'");
}
 
[EntityInputHandler<CBaseDoor>("Open")]
public void OnDoorOpenInputGeneric(IOnEntityIdentityAcceptInputHookEvent @event)
{
}

Set @event.Result = HookResult.Stop to block the input from being accepted.

APIServerNetMessageHandler

Hooks a net message the server sends to clients - see APIINetMessageService and Net Messages:

[ServerNetMessageHandler]
public HookResult OnServerSound(CMsgSosStartSoundEvent msg)
{
    Console.WriteLine($"sound hash={msg.SoundeventHash}");
    return HookResult.Continue;
}

APIServerNetMessageInternalHandler

Same as APIServerNetMessageHandler, but for internal server messages, which carry a target playerId:

[ServerNetMessageInternalHandler]
public HookResult OnServerSoundInternal(CMsgSosStartSoundEvent msg, int playerId)
{
    Console.WriteLine($"player={playerId}, sound hash={msg.SoundeventHash}");
    return HookResult.Continue;
}

APIClientNetMessageHandler

Hooks a net message a client sends to the server:

[ClientNetMessageHandler]
public HookResult OnClientMove(CCLCMsg_Move msg, int playerId)
{
    Console.WriteLine($"player={playerId}, lastCmd={msg.LastCommandNumber}");
    return HookResult.Continue;
}

See Net Messages and Entity for the programmatic (Core.NetMessage/Core.EntitySystem) equivalents of the attributes above.