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