` from core are injected through constructor.
For more information, please see [MSLearn](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection).
You will also need to add `Microsoft.Extensions.DependencyInjection` to your plugin.
## https://swiftlys2.net/docs/guides/terminologies
---
title: Terminologies
---
## Environments
### Managed
Managed is .NET managed code which is used by C++ core. The memory and lifetime of objects are managed automatically by the runtime (garbage collection, type safety, metadata, etc.). The managed environment hosts and runs the plugins.
### Native
Native is the unmanaged side of SwiftlyS2, to be more exact, the C++ core. Memory management and execution are done manually (new/delete, RAII, etc.). The C++ core is the environment that initialized the managed environment for plugins.
## Players
All players in CS2 are split between a player controller & a player pawn.
### Controllers
The player controller represents the player on the server. It holds informations about the player, such as: name, steamid, etc. Every controller has assigned a pawn. In the entity system, it holds the indexes from 1 to 64. The controller is a virtual entity that represents a player abstraction, so stuff such like: health, positioning, velocity can't be modified from over here.
### Pawn
The player pawn represents the character of the player on the server. It holds informations about the character in game, such as: health, position, velocity, color, weapons and so on. Every pawn has a controller assigned to it.
### Slot/PlayerId
A slot on the server represents the controller's entity index - 1, so for example, if the controller entity index is 7, the player has assigned slot 6. Most of the game events are referring to the slot as `userid` or `playerid`.
### Player Object
The player object is an object created by us to give the possibility to developers to have faster accessing to game elements such as: fetching controller, fetching pawn, teleporting pawn, sending messages and so on. This allows us to give efficient API for a faster development experience.
## Entities
The entity system has a maximum capacity of 16384 indexes. Each entity has assigned a specific index.
Ranges:
- `1-64`: Player Controllers (controller index = playerid + 1)
- `65-16383`: Entites
### Handles
An entity handle is a safe container in which an entity is stored. It stores in the container the pointer of the entity and the entity index. Using that, you can ensure that your entity is safely stored and that it can be checked against a list from the entity system to see if the said entity is still valid.
### Kinds
#### Temporary
Temporary entities are spawn every round, at the beginning of the round and are removed at the end of the round.
#### Permanent
Permanent entities are spawned with the map, and those are not removed once the round ends.
## https://swiftlys2.net/docs/guides/chat-and-html-styling
---
title: Chat & CenterHTML Styling
---
# Chat Colors
Chat colors provide a simple way to colorize text using bracket syntax. This works in chat messages and other text outputs.
## Syntax
Use square brackets with color names to change text color:
```csharp
var message = "[red]This is red [lime]This is lime [default]Back to default";
player.SendChat(message);
```
Use `[default]` or `[/]` to reset text back to the default color.
### Available Chat Colors
white
darkred
green
lightyellow
lightblue
olive
lime
red
lightpurple
purple
grey
yellow
gold
silver
blue
darkblue
bluegrey
magenta
lightred
orange
### Chat Color Examples
Using translations with placeholders for dynamic content:
```csharp
// Simple colored message
player.SendChat(Core.Localizer["chat.welcome"]);
// Multiple colors with player name
player.SendChat(Core.Localizer["chat.player_joined", player.Name]);
// Error message with reset
player.SendChat(Core.Localizer["chat.error", "Something went wrong"]);
// Team score announcement
Core.PlayerManager.SendChat(Core.Localizer["chat.team_scores", ctScore, tScore]);
```
```json
{
"chat.welcome": "[green]Welcome to the server!",
"chat.player_joined": "[gold]Player [lime]{0}[gold] has joined!",
"chat.error": "[red]ERROR: [/]{0}",
"chat.team_scores": "[blue]CT Team: [yellow]{0} [default]- [orange]T Team: [yellow]{1}"
}
```
---
# CenterHTML Styling
## Syntax
Panorama UI uses a different syntax than standard HTML for styling elements.
Browse all available Panorama UI stylings in the [CS2 data tracking
repository](https://github.com/SteamDatabase/GameTracking-CS2/tree/master/game/core/pak01_dir/panorama/styles)
`.css` files.
## Supported HTML Tags
Panorama UI supports most standard HTML tags:
div
span
p
a
img
br
hr
h1-h6
strong
em
b
i
u
pre
### Inline Properties
```csharp
// ✅ Correct - Direct property attributes
var element = "Red Text";
```
```html
Red Text
```
Use property names directly as attributes instead of wrapping them in a
`style` attribute.
### CSS Classes
Apply built-in Panorama UI classes using the standard `class` attribute:
```csharp
// Single class
var element = "Medium Font";
// Multiple classes
var element = "Critical text";
```
## Common Styling Options
### Colors
For HTML elements, use the `color` attribute with color names or hex codes.
The same color names from [Chat Colors](#chat-colors) work in HTML:
```csharp
var message = "Red Text";
```
white
darkred
green
lightyellow
lightblue
olive
lime
red
lightpurple
purple
grey
yellow
gold
silver
blue
darkblue
bluegrey
magenta
lightred
orange
HTML elements also support hex color codes for custom colors:
```csharp
var ctBlue = "CT Blue";
var tOrange = "T Orange";
var custom = "Custom Color";
```
### Custom Color Picker
Try out different colors and copy the hex code to use in your HTML:
### Font Size Classes
| Class | Size |
| -------------- | ----------------- |
| `fontSize-xs` | Extra Small |
| `fontSize-sm` | Small |
| `fontSize-m` | Medium |
| `fontSize-l` | Large |
| `fontSize-xl` | Extra Large |
| `fontSize-xxl` | Extra Extra Large |
### Font Style Classes
| Class | Effect |
| ----------------- | ------------------------ |
| `fontStyle-m` | Medium font style |
| `fontWeight-bold` | Bold text |
| `CriticalText` | Critical/warning styling |
## Practical Examples
### Basic Usage
```csharp
var message = "This text is red";
Core.PlayerManager.SendCenterHTML(message, 5);
```
```csharp
var message = "Large Green Text";
Core.PlayerManager.SendCenterHTML(message, 5);
```
### Player Status Display
Show ready player counts with dynamic colors:
```csharp
var readyPlayers = 5;
var playersToStart = 10;
Core.PlayerManager.SendCenterHTML(
Core.Localizer["centerhtml.ready_status", readyPlayers, playersToStart], 10);
```
```json
{
"centerhtml.ready_status": "[{0}/{1}]
Match starts when {1} players are ready!"
}
```
### Multi-line Formatted Messages
Create structured messages with titles and lists:
```csharp
Core.PlayerManager.SendCenterHTML(
Core.Localizer["centerhtml.server_rules"], 15);
```
```json
{
"centerhtml.server_rules": "Server Rules
• Be respectful to other players
• No cheating or exploiting
• Have fun!"
}
```
### Dynamic Status Indicators
```csharp
var player = Core.PlayerManager.GetPlayer(1);
var status = "ready";
var statusColor = status == "ready" ? "green" : "red";
player.SendCenterHTML(
Core.Localizer["centerhtml.player_status", player.Name, statusColor, status], 5);
```
```json
{
"centerhtml.player_status": "{0} is {2}"
}
```
### Progress Bar with Characters
```csharp
var progress = 75; // 75%
var filled = progress / 10;
var empty = 10 - filled;
var bar = new string('█', filled) + new string('░', empty);
Core.PlayerManager.SendCenterHTML(
Core.Localizer["centerhtml.loading_bar", bar, progress], 3);
```
```json
{
"centerhtml.loading_bar": "Loading: {0} {1}%"
}
```
### Countdown Timer
```csharp
var timeLeft = 30;
var color = timeLeft <= 5 ? "red" : timeLeft <= 10 ? "yellow" : "green";
Core.PlayerManager.SendCenterHTML(
Core.Localizer["centerhtml.countdown", color, timeLeft], 1);
```
```json
{
"centerhtml.countdown": "Round starts in
{1}"
}
```
### Team Score Display
```csharp
var ctScore = 8;
var tScore = 6;
Core.PlayerManager.SendCenterHTML(
Core.Localizer["centerhtml.team_scores", ctScore, tScore], 5);
```
```json
{
"centerhtml.team_scores": "CT: {0} - T: {1}"
}
```
## Best Practices
**Remember:** Panorama UI's HTML parsing has limitations. Always test
complex layouts in-game to ensure they render correctly.
1. **Keep it Simple**: Complex nested structures may not render as expected
2. **Test Colors**: Verify color visibility against different background themes
3. **Use Classes**: Prefer built-in classes over inline properties for consistency
4. **Duration Matters**: Set appropriate display durations for message complexity
5. **String Interpolation**: Use `$""` for clean, readable message construction
## Reference
### Common Inline Properties
- `color` - Text color (color names or hex codes)
- `font-size` - Font size (prefer classes)
- `font-weight` - Font weight (prefer classes)
### Finding More Classes
Explore the [CS2 Panorama
styles](https://github.com/Swiftly-Tracker/CS2-Dumps/tree/main/install/game/csgo/pak01/panorama/styles)
to discover additional classes and styling options. New classes are added
with CS2 updates.
## https://swiftlys2.net/docs/guides/porting-from-css
---
title: Porting from CounterStrikeSharp
---
# Porting from CounterStrikeSharp
This guide will help you migrate your existing CounterStrikeSharp plugins to SwiftlyS2. We'll cover the key differences between the frameworks and provide side-by-side examples to make the transition as smooth as possible.
Both frameworks share a lot of the same underlying concepts (game events,
schema access, virtual function hooking), so most of the migration work is
renaming calls to their SwiftlyS2 equivalent rather than rethinking your
plugin's logic.
## Overview
SwiftlyS2 introduces several key improvements over CounterStrikeSharp:
- **Modern .NET 10** - Takes advantage of the latest .NET features and performance improvements
- **Improved API Design** - More intuitive and consistent API structure, split into discrete `Core.*` services
- **Built-in Menu System** - Buttons, sliders and inputs out of the box, no extra dependency needed
- **Database Abstraction** - Global connection configuration shared across plugins
- **Per-Plugin GameData** - Each plugin manages its own signatures/offsets instead of relying on a shared gamedata file
- **Cross-Plugin Interfaces** - A capability-style system for exposing and consuming APIs between plugins
## Migration Checklist
Before starting your migration, ensure you have:
- [ ] .NET 10.0 SDK installed
- [ ] Your existing CounterStrikeSharp plugin source code
- [ ] Understanding of your plugin's dependencies
- [ ] Test environment set up for SwiftlyS2
## Project Setup
### Package Reference
```xml
```
```xml
```
**Version patterns:**
- `1.0.3` - Specific version
- `*` - Latest stable release
- `*-*` - Latest preview/pre-release version
### Target Framework
```xml
net8.0
```
```xml
net10.0
```
SwiftlyS2 requires .NET 10.0 or later. Make sure you have the appropriate
SDK installed.
## Plugin Class
### Base Structure
```csharp
public class MyPlugin : BasePlugin
{
public override string ModuleName => "My Plugin";
public override string ModuleVersion => "1.0.0";
public override void Load(bool hotReload)
{
}
}
```
```csharp
[PluginMetadata(Id = "my.plugin", Version = "1.0.0", Name = "My Plugin", Author = "Author")]
public sealed class Plugin(ISwiftlyCore core) : BasePlugin(core)
{
public static new ISwiftlyCore Core { get; private set; } = null!;
public override void Load(bool hotReload)
{
Core = base.Core;
}
public override void Unload()
{
}
}
```
SwiftlyS2 also calls `OnAllPluginsLoaded()` once every plugin has finished
loading - useful if you need another plugin's shared interface to already be
registered before you use it.
## Event Handling (Game Events)
### Attribute-Based Registration (Recommended)
```csharp
RegisterEventHandler(OnPlayerDeath);
RegisterEventHandler(OnPlayerDeath, HookMode.Pre);
```
```csharp
[GameEventHandler(HookMode.Post)]
private HookResult OnPlayerDeath(EventPlayerDeath @event)
{
var player = @event.UserIdPlayer;
return HookResult.Continue;
}
[GameEventHandler(HookMode.Pre)]
private HookResult OnPlayerDeathPre(EventPlayerDeath @event)
{
return HookResult.Continue;
}
```
### Manual Registration
In SwiftlyS2, you can also register event handlers manually, typically in `Load()`:
```csharp
// SwiftlyS2 - manual registration in Load()
Guid postHookId = Core.GameEvent.HookPost(OnPlayerDeath);
Guid preHookId = Core.GameEvent.HookPre(OnPlayerDeathPre);
// Unhook later, e.g. in Unload()
Core.GameEvent.Unhook(postHookId);
```
### Event Handler Signature
```csharp
private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
{
var player = @event.Userid;
return HookResult.Continue;
}
```
```csharp
private HookResult OnPlayerDeath(EventPlayerDeath @event)
{
var player = @event.UserIdPlayer;
return HookResult.Continue;
}
```
In SwiftlyS2, the `GameEventInfo` parameter is removed. Event fields are
read through typed properties, or through `@event.Accessor` if a typed
property is inexistent.
### Getting Player from Event
```csharp var player = @event.Userid; var attacker = @event.Attacker;
```
```csharp var player = @event.Accessor.GetPlayer("userid"); var attacker
= @event.Accessor.GetPlayer("attacker"); ```
## Player Operations
### Getting All Players
```csharp
var players = Utilities.GetPlayers();
```
```csharp
var players = Core.PlayerManager.GetAllPlayers();
// Get a single player by slot/index
var player = Core.PlayerManager.GetPlayer(playerId);
```
### Player Properties
```csharp var isValid = player.IsValid; var name = player.PlayerName;
var steamId = player.SteamID; ```
```csharp var isValid = player.IsValid; var name = player.Name; //
convenience wrapper around player.Controller?.PlayerName var steamId =
player.SteamID; ```
### Sending Messages
```csharp
// Send to specific player
player.PrintToChat("message");
// Send to all players
Server.PrintToChatAll("message");
```
```csharp
// Send to specific player
player.SendChat("message");
// Send to all players
Core.PlayerManager.SendChat("message");
```
### Additional Message Types
SwiftlyS2 provides multiple message methods for enhanced communication:
```csharp
// Additional message types in SwiftlyS2 (also available per-player on IPlayer)
Core.PlayerManager.SendCenter("center message");
Core.PlayerManager.SendAlert("alert message");
Core.PlayerManager.SendConsole("console message");
Core.PlayerManager.SendCenterHTML("HTML", duration: 5000); // duration in milliseconds
```
## Chat Colors
SwiftlyS2 uses square brackets instead of curly braces for colors:
```csharp $"{ChatColors.Red}Red text {ChatColors.Green}Green text" ```
```csharp "[red]Red text [green]Green text" ```
See the [Chat & CenterHTML Styling](/docs/guides/chat-and-html-styling) guide for the full color list.
## Timers & Scheduling
SwiftlyS2's tick-based `Delay` overload uses game ticks as the unit. Use the
`*BySeconds` variants for time-based delays.
```csharp
AddTimer(5.0f, () => { /* code */ });
AddTimer(1.0f, () => { /* code */ }, TimerFlags.REPEAT);
Server.NextFrame(() => { /* code */ });
Server.NextWorldUpdate(() => { /* code */ });
```
```csharp
Core.Scheduler.Delay(64, () => { /* code */ }); // delay in ticks
Core.Scheduler.DelayBySeconds(5, () => { /* code */ }); // delay in seconds
Core.Scheduler.DelayAndRepeatBySeconds(5, 1, () => { /* code */ }); // initial delay + repeat interval
Core.Scheduler.NextTick(() => { /* code */ }); // next server tick
Core.Scheduler.NextWorldUpdate(() => { /* code */ }); // next world update frame
```
Both `Delay` and `DelayAndRepeatBySeconds` return a `CancellationTokenSource` you can use to cancel the scheduled task early.
## Commands
### Basic Command Registration
```csharp
[ConsoleCommand("css_mycommand", "Description")]
public void OnMyCommand(CCSPlayerController? player, CommandInfo command)
{
}
```
```csharp
[Command("mycommand")]
public void OnMyCommand(ICommandContext context)
{
var player = context.Sender;
}
```
### Admin Commands
In CounterStrikeSharp, permissions are declared with a separate `[RequiresPermissions]` attribute. In SwiftlyS2, permissions are part of the `Command` attribute itself:
```csharp
[Command("admincommand", permission: "admin.permission")]
public void OnAdminCommand(ICommandContext context)
{
// Only players with admin.permission can use this command
}
```
### Command Aliases
SwiftlyS2 provides a clean way to register command aliases:
```csharp
// Register primary command with aliases
Core.Command.RegisterCommand("mycommand", OnMyCommand);
Core.Command.RegisterCommandAlias("mycommand", "mc");
Core.Command.RegisterCommandAlias("mycommand", "mycmd");
```
## Permissions
CounterStrikeSharp ships a full admin framework (`admins.json`, `admin_groups.json`, immunity, overrides). SwiftlyS2's built-in permission manager is intentionally simpler - a flat, wildcard-aware permission store keyed by SteamID that plugins (or an admin plugin built on top of it) can manage.
```csharp
bool isAdmin = AdminManager.PlayerHasPermissions(player, "@css/ban");
```
```csharp
bool isAdmin = Core.Permission.PlayerHasPermission(player.SteamID, "plugin.ban");
// Supports 'xxx.*' wildcards, multi-permission checks, and grant/revoke:
Core.Permission.PlayerHasPermissions(player.SteamID, ["plugin.ban", "plugin.kick"]);
Core.Permission.AddPermission(player.SteamID, "plugin.ban");
Core.Permission.RemovePermission(player.SteamID, "plugin.ban");
```
`Command`'s `permission` parameter checks against this same permission
store, so you rarely need to call `Core.Permission` directly inside a
command handler.
## Menus
SwiftlyS2 has a built-in full menu system with support for buttons, inputs, sliders, and more.
### Creating a Menu
```csharp
var menu = new ChatMenu("Title");
menu.AddMenuOption("Option 1", (p, o) => { });
MenuManager.OpenChatMenu(player, menu);
```
```csharp
var builder = Core.MenusAPI
.CreateBuilder()
.Design.SetMenuTitle("Title")
.Design.SetMenuTitleVisible(true)
// All options below are optional
.Design.SetMenuFooterVisible(true)
.Design.SetGlobalScrollStyle(MenuOptionScrollStyle.LinearScroll) // doesn't wrap when reaching the end
.SetAutoCloseDelay(10) // auto close after 10 seconds
.SetSelectButton(KeyBind.Space) // button to select option
.SetPlayerFrozen(false); // don't freeze player while menu is open
var button = new ButtonMenuOption("Option 1");
button.Click += (sender, args) =>
{
var clickedBy = args.Player;
// handle click
return ValueTask.CompletedTask;
};
builder.AddOption(button);
var menu = builder.Build();
Core.MenusAPI.OpenMenuForPlayer(player, menu);
```
### Closing Menus
```csharp MenuManager.CloseActiveMenu(player); ```
```csharp Core.MenusAPI.CloseMenuForPlayer(player, menu); // or, without
holding a reference to the built menu:
Core.MenusAPI.CloseActiveMenu(player); ```
## Cross-Plugin Capabilities
CounterStrikeSharp lets plugins expose an API to each other through `PluginCapability`. SwiftlyS2 has the same idea built directly into `BasePlugin`, backed by `IInterfaceManager`.
```csharp
// Provider plugin
public static readonly PluginCapability Capability = new("my_plugin:api");
Capabilities.RegisterPluginCapability(Capability, () => new MyPluginApi());
// Consumer plugin
var api = MyPluginProvider.Capability.Get();
```
```csharp
// Provider plugin
public override void ConfigureSharedInterface(IInterfaceManager interfaceManager)
{
interfaceManager.AddSharedInterface("my.plugin:api", new MyPluginApi());
}
// Consumer plugin - runs after every plugin's ConfigureSharedInterface has fired
public override void UseSharedInterface(IInterfaceManager interfaceManager)
{
if (interfaceManager.TryGetSharedInterface("my.plugin:api", out var api))
{
// use api
}
}
```
The shared interface type (`IMyPluginApi` above) must live somewhere both
plugins can reference, e.g. a small shared contracts assembly - the same
requirement CounterStrikeSharp has for `PluginCapability<T>`
interfaces.
## Entity Operations
### Finding Entities
```csharp
Utilities.FindAllEntitiesByDesignerName("planted_c4");
```
```csharp
Core.EntitySystem.GetAllEntities(); // all entities
Core.EntitySystem.GetAllEntitiesByClass(); // by class type
Core.EntitySystem.GetAllEntitiesByDesignerName("planted_c4"); // by designer name
```
### Modifying Schema Values
In CSS you need to call `SetStateChanged` after modifying networked properties. In SwiftlyS2 every networked schema field gets a generated `Updated()` method instead.
```csharp player.PlayerPawn.Value.Health = 100;
Utilities.SetStateChanged(player.PlayerPawn.Value, "CBaseEntity",
"m_iHealth"); ```
```csharp var pawn = player.PlayerPawn; pawn.Health = 100;
pawn.HealthUpdated(); ```
## Configuration
SwiftlyS2 uses the .NET Options pattern with support for JSON, JSONC, and TOML files with hot-reload capability.
### Loading Configuration
```csharp
// SwiftlyS2
private PluginConfig _config = null!;
private void LoadConfiguration()
{
const string ConfigFileName = "config.json";
const string ConfigSection = "MyPlugin";
// Initialize config file from model (creates file with defaults if not exists)
Core.Configuration
.InitializeJsonWithModel(ConfigFileName, ConfigSection)
// Configure the config source with hot-reload support
.Configure(cfg => cfg.AddJsonFile(
Core.Configuration.GetConfigPath(ConfigFileName),
optional: false,
reloadOnChange: true)); // auto-reload on file change
// Build service provider with options pattern
ServiceCollection services = new();
services.AddSwiftly(Core)
.AddOptionsWithValidateOnStart()
.BindConfiguration(ConfigSection);
var provider = services.BuildServiceProvider();
_config = provider.GetRequiredService>().Value;
}
```
### Config Model Example
```csharp
public sealed class PluginConfig
{
public string DatabaseConnection { get; set; } = "";
public int MaxPlayers { get; set; } = 10;
public bool EnableFeature { get; set; } = true;
}
```
The generated `config.json` will look like:
```json
{
"MyPlugin": {
"DatabaseConnection": "",
"MaxPlayers": 10,
"EnableFeature": true
}
}
```
See the [Dependency Injection](/docs/guides/dependency-injection) guide for more on `services.AddSwiftly(Core)`.
## Database
CounterStrikeSharp leaves database access entirely up to the plugin (usually Dapper/MySqlConnector with a connection string in the plugin's own config). SwiftlyS2 has a global database configuration system instead - connections are defined once in `configs/database.jsonc` and referenced by name from any plugin.
**Supported databases:** MySQL, PostgreSQL, SQLite
### Global Configuration
Create a `configs/database.jsonc` file in your SwiftlyS2 installation:
```json
{
"default_connection": "host",
"connections": {
"host": "mysql://username:password@localhost:3306/database"
}
}
```
**Connection string formats:**
- MySQL: `mysql://user:pass@host:3306/database`
- PostgreSQL: `postgresql://user:pass@host:5432/database`
- SQLite: `sqlite://path/to/database.db`
### Using in Plugins
```csharp
// Plugin config references connection by name
public sealed class PluginConfig
{
public string DatabaseConnection { get; set; } = "host"; // name from database.jsonc
}
// Get connection using the name
using var connection = Core.Database.GetConnection(_config.DatabaseConnection);
connection.Open();
// Use with Dapper or any ADO.NET approach
await connection.ExecuteAsync("SELECT * FROM players WHERE steamid = @SteamId", new { SteamId = steamId });
```
## Translations
### File Location
``` lang/en.json ```
``` resources/translations/en.jsonc ```
### Usage
```csharp
// Server language
Localizer["key"]
Localizer["key", arg1, arg2]
// Player language
Localizer.ForPlayer(player, "key")
Localizer.ForPlayer(player, "key", arg1, arg2)
```
```csharp
// Server language
Core.Localizer["key"]
Core.Localizer["key", arg1, arg2]
// Player language
var localizer = Core.Translation.GetPlayerLocalizer(player);
localizer["key"]
localizer["key", arg1, arg2]
```
## Logging
```csharp Logger.LogInformation("Loaded {Plugin}", ModuleName); ```
```csharp Core.Logger.LogInformation("Loaded {Plugin}", "my.plugin");
```
`Core.Logger` is a standard `Microsoft.Extensions.Logging.ILogger`, and `Core.LoggerFactory` is available if you need to create scoped loggers for other classes.
## Project File (.csproj)
Full example with comments:
```xml
net10.0
enable
enable
latest
true
$(MSBuildThisFileName)
$(MSBuildThisFileName)
false
true
true
$(MSBuildThisFileDirectory)build/
$(OutputPath)publish/$(MSBuildThisFileName)
```
## ConVars
### Finding and Reading ConVars
```csharp
var convar = ConVar.Find("mp_round_restart_delay");
var value = convar?.GetPrimitiveValue();
```
```csharp
var convar = Core.ConVar.Find("mp_round_restart_delay");
var value = convar?.Value;
```
### Setting ConVar Values
```csharp
var convar = ConVar.Find("mp_warmup_time");
convar?.SetValue(30);
```
```csharp
var convar = Core.ConVar.Find("mp_warmup_time");
if (convar != null)
convar.Value = 30;
```
### Creating Plugin ConVars
CounterStrikeSharp exposes this through the `[ConVar]` attribute on a field. SwiftlyS2 creates (or reuses) one imperatively:
```csharp
[ConVar("my_plugin_enabled", "Enables the plugin", ConVarFlags.FCVAR_NONE)]
public bool Enabled { get; set; } = true;
```
```csharp
var enabled = Core.ConVar.CreateOrFind("my_plugin_enabled", "Enables the plugin", true);
bool isEnabled = enabled.Value;
```
## Listeners
SwiftlyS2 uses typed event handlers with strongly-typed event parameters.
### Client Events
```csharp
RegisterListener((slot, name, ip) => { });
RegisterListener((slot) => { });
RegisterListener((slot, steamId) => { });
```
```csharp
// Typed event handlers (recommended)
private void OnClientConnected(IOnClientConnectedEvent @event)
{
// @event.PlayerId, can set @event.Result = HookResult.Stop to prevent join
}
private void OnClientDisconnected(IOnClientDisconnectedEvent @event)
{
// @event.PlayerId, @event.Reason
}
private void OnClientSteamAuthorize(IOnClientSteamAuthorizeEvent @event)
{
// @event.PlayerId
}
private void OnClientPutInServer(IOnClientPutInServerEvent @event)
{
// Player fully connected and ready
}
// Register in Load():
Core.Event.OnClientConnected += OnClientConnected;
Core.Event.OnClientDisconnected += OnClientDisconnected;
Core.Event.OnClientSteamAuthorize += OnClientSteamAuthorize;
Core.Event.OnClientPutInServer += OnClientPutInServer;
```
### Map Events
```csharp
RegisterListener((mapName) => { });
RegisterListener(() => { });
```
```csharp
private void OnMapLoad(IOnMapLoadEvent @event)
{
var mapName = @event.MapName;
}
private void OnMapUnload(IOnMapUnloadEvent @event)
{
var mapName = @event.MapName;
}
// Register in Load():
Core.Event.OnMapLoad += OnMapLoad;
Core.Event.OnMapUnload += OnMapUnload;
```
### Entity Events
```csharp
RegisterListener((entity) => { });
RegisterListener((entity) => { });
RegisterListener((entity) => { });
```
```csharp
private void OnEntityCreated(IOnEntityCreatedEvent @event)
{
var entity = @event.Entity;
}
private void OnEntitySpawned(IOnEntitySpawnedEvent @event)
{
var entity = @event.Entity;
}
private void OnEntityDeleted(IOnEntityDeletedEvent @event)
{
var entity = @event.Entity;
}
// Register in Load():
Core.Event.OnEntityCreated += OnEntityCreated;
Core.Event.OnEntitySpawned += OnEntitySpawned;
Core.Event.OnEntityDeleted += OnEntityDeleted;
```
### Entity Take Damage
`Core.Event.OnEntityTakeDamage` still exists but is deprecated. Use
`Core.GameHooks.Entities.TakeDamage` instead, which gives you separate
Pre/Post hooks and direct access to the native damage info.
```csharp
private void OnTakeDamagePre(ref TakeDamageEntityPreContext ctx)
{
var entity = ctx.Params.Entity;
ref var info = ref ctx.Params.Info;
// inspect/modify info, then optionally: ctx.SetHookResult(HookResult.Stop);
}
// Register in Load():
Core.GameHooks.Entities.TakeDamage.Pre += OnTakeDamagePre;
```
### Other Useful Events
```csharp
// SwiftlyS2 additional events
private void OnTick() { } // every server tick (hot path!)
private void OnWorldUpdate() { } // every world update, even in hibernation
private void OnPrecacheResource(IOnPrecacheResourceEvent @event) { @event.AddItem("path/to/resource.vmdl"); }
// Register in Load():
Core.Event.OnTick += OnTick;
Core.Event.OnWorldUpdate += OnWorldUpdate;
Core.Event.OnPrecacheResource += OnPrecacheResource;
```
## Player Movement
### Teleportation
```csharp player.PlayerPawn.Value?.Teleport(position, angles, velocity);
```
```csharp player.Teleport(position, angles, velocity); // or with
Vector.Zero for no velocity change player.Teleport(position, angles,
Vector.Zero); ```
### Reading Velocity and Position
```csharp var velocity = player.PlayerPawn.Value?.AbsVelocity; var
position = player.PlayerPawn.Value?.AbsOrigin; var angles =
player.PlayerPawn.Value?.EyeAngles; ```
```csharp var pawn = player.PlayerPawn; var velocity =
pawn?.AbsVelocity; var position =
pawn?.CBodyComponent?.SceneNode?.AbsOrigin; var angles =
pawn?.EyeAngles; ```
## GameData & Function Hooking
SwiftlyS2 provides a powerful system for working with game signatures, offsets, and function hooking. GameData files are stored in each plugin's own `resources/gamedata/` folder, allowing each plugin to manage its own signatures and offsets independently - unlike CounterStrikeSharp, which relies on a single shared `gamedata.json`.
This is an advanced topic. Most plugins won't need to use function hooking
directly.
### Delegate Definition
In SwiftlyS2, you need to define delegates that match the native function signatures:
```csharp
// CSS - no explicit delegate needed, uses DynamicHook
// Hook handlers receive DynamicHook for parameter extraction
```
```csharp
// SwiftlyS2 - define delegate matching the native function signature
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate nint GiveNamedItemDelegate(nint pItemServices, nint weaponName);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void FindMatchingWeaponsDelegate(nint pPawn, nint weaponName, int team, byte searchInventory, nint outVector);
```
### Signature Lookup
```csharp
var funcAddress = GameData.GetSignature("CCSPlayer_FindMatchingWeaponsForTeamLoadout");
var func = new MemoryFunctionVoid(funcAddress);
func.Invoke(player.PlayerPawn.Value.Handle, weaponName, (int)team, false, vectorPtr);
```
```csharp
if (Core.GameData.TryGetSignature("CCSPlayer_FindMatchingWeaponsForTeamLoadout", out var address))
{
var func = Core.Memory.GetUnmanagedFunctionByAddress(address);
func.Invoke(pPawn, weaponName, team, searchInventory, outVector);
}
```
### VTable Hooking
```csharp
VirtualFunctions.GiveNamedItemFunc.Hook(OnGiveNamedItemPre, HookMode.Pre);
VirtualFunctions.GiveNamedItemFunc.Hook(OnGiveNamedItemPost, HookMode.Post);
```
```csharp
private IUnmanagedFunction? _giveNamedItem;
private Guid _giveNamedItemHookId;
// In Load() or initialization
if (Core.GameData.TryGetOffset("CCSPlayer_ItemServices::GiveNamedItem", out var offset))
{
var vtable = Core.Memory.GetVTableAddress("server", "CCSPlayer_ItemServices");
if (vtable.HasValue)
{
_giveNamedItem = Core.Memory.GetUnmanagedFunctionByVTable(vtable.Value, (int)offset);
_giveNamedItemHookId = _giveNamedItem.AddHook(OnGiveNamedItemHook);
}
}
```
### Hook Handler Implementation
```csharp
// CSS - separate Pre and Post handlers
private HookResult OnGiveNamedItemPre(DynamicHook hook)
{
var itemServices = hook.GetParam(0);
string classname = hook.GetParam(1);
return HookResult.Continue;
}
private HookResult OnGiveNamedItemPost(DynamicHook hook)
{
// Post-execution logic
return HookResult.Continue;
}
```
```csharp
// SwiftlyS2 - single handler with callNext pattern
private GiveNamedItemDelegate OnGiveNamedItemHook(Func callNext)
{
return (pItemServices, weaponName) =>
{
// Pre-hook logic here
var classname = Marshal.PtrToStringUTF8(weaponName);
// Call original function
var result = callNext()(pItemServices, weaponName);
// Post-hook logic here
return result;
};
}
```
### Hook Cleanup
```csharp VirtualFunctions.GiveNamedItemFunc.Unhook(OnGiveNamedItemPre,
HookMode.Pre);
VirtualFunctions.GiveNamedItemFunc.Unhook(OnGiveNamedItemPost,
HookMode.Post); ```
```csharp // SwiftlyS2 - in Unload() if (_giveNamedItem != null &&
_giveNamedItemHookId != Guid.Empty)
_giveNamedItem.RemoveHook(_giveNamedItemHookId); ```
## Migration Tips & Best Practices
### Start Small
Begin by migrating basic functionality first:
1. Plugin structure and metadata
2. Basic commands and events
3. Player operations and messaging
4. Configuration and database connections
5. Advanced features like menus and function hooking
### Common Migration Patterns
#### Pattern: Update Package References
Always update your `.csproj` file first:
```xml
```
#### Pattern: Core Access
Replace global utility classes with Core services:
```csharp
// Old CSS pattern
Utilities.GetPlayers().ForEach(player => { });
// New SwiftlyS2 pattern
Core.PlayerManager.GetAllPlayers().ForEach(player => { });
```
#### Pattern: Event Registration
Use attributes for cleaner event handling:
```csharp
// Instead of manual registration in Load()
// Core.GameEvent.HookPost(OnPlayerDeath);
// Use attributes (recommended)
[GameEventHandler(HookMode.Post)]
private HookResult OnPlayerDeath(EventPlayerDeath @event) { return HookResult.Continue; }
```
### Testing Your Migration
1. **Compile First**: Ensure your plugin compiles without errors
2. **Test Basic Functions**: Verify commands, events, and player operations work
3. **Check Resources**: Ensure translations and config files are properly located
4. **Test Hot Reload**: SwiftlyS2 has improved hot reload support
### Getting Help
- **Documentation**: Check the SwiftlyS2 API documentation for detailed reference
- **Examples**: Look at the plugin template examples in the `examples/` folder
- **Community**: Join the SwiftlyS2 Discord or forums for community support
Migration from CounterStrikeSharp to SwiftlyS2 typically results in cleaner,
more maintainable code with better performance and more features.
## https://swiftlys2.net/docs/resources/cli-options
---
title: CLI Options
---
SwiftlyS2 startup behavior can be configured with command-line options passed when launching the game server.
## Startup Example
```bash
./game/bin/win64/cs2.exe -dedicated +map de_dust2 \
-sw_path addons/swiftlys2 \
-sw_logpath addons/swiftlys2/logs \
-sw_hide_logs_in_console 1 \
-sw_loglevel WARNING
```
## Options Reference
| Option | Example | Details |
| -------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `-sw_path` | `-sw_path addons/swiftlys2` | Relative path from `game/csgo` where SwiftlyS2 is located. Default: `addons/swiftlys2`. |
| `-sw_logpath` | `-sw_logpath addons/swiftlys2/logs` | Relative path from `game/csgo` where logs are stored. Default: `addons/swiftlys2/logs`. |
| `-sw_hide_logs_in_console` | `-sw_hide_logs_in_console 1` | Disables plugin logs in console. Accepted values: `1`, `TRUE`, `YES`. |
| `-sw_loglevel` | `-sw_loglevel WARNING` | Minimum log level shown in console. Default: all. Accepted values: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `OFF`. |
## Notes
- `-sw_path` and `-sw_logpath` are relative to the `game/csgo` directory.
- Use `-sw_hide_logs_in_console` when you want plugin logs to remain in files but not in console output.
- Use `-sw_loglevel` to reduce console noise while preserving higher-severity logs.
## https://swiftlys2.net/docs/resources/command-overrides
---
title: Command Overrides
---
Command overrides allow you to remap permission checks for existing commands without changing plugin code.
## Config File
Edit the following file:
`addons/swiftlys2/configs/command_overrides.jsonc`
```jsonc
{
"CommandOverrides": {
"Permissions": {
"sw_abc": "test.permission"
}
}
}
```
## How It Works
- `sw_abc` is the command name.
- `test.permission` is the new permission that command will require.
- Add more command-to-permission pairs under `CommandOverrides.Permissions` as needed.
## Notes
- Use exact command names when defining overrides.
- Overrides only change required permissions; command behavior remains unchanged.
- Keep this file under source control to make permission policy changes easy to audit.
## https://swiftlys2.net/docs/resources/console-filter
---
title: Console Filter
---
The console filter suppresses known noisy console lines so important logs remain visible.
## Config File
Edit the following file:
`addone/swiftlys2/configs/confilter.jsonc`
This file is a JSONC object where each entry defines one filter rule.
- Key: rule name used to identify the filter.
- Value: text or pattern to match in console output.
- When output matches a configured rule, it is filtered out.
## Command Management
Use `sw confilter` with the following subcommands:
| Subcommand | Description |
| ---------- | -------------------------------------- |
| `enable` | Enable console filtering. |
| `disable` | Disable console filtering. |
| `status` | Show the status of the console filter. |
| `reload` | Reload console filter configuration. |
## Typical Workflow
1. Update `confilter.jsonc` with the rules you want.
2. Run `sw confilter reload` to apply configuration changes.
3. Use `sw confilter status` to confirm filtering is active.
## https://swiftlys2.net/docs/resources/core-config
---
title: Core Configuration
---
Core configuration controls framework-wide behavior such as command parsing, plugin loading, menu input, console filtering, console logging, unlocker toggles, and Steam auth mode.
## Config File
Edit the following file:
`addons/swiftlys2/configs/core.jsonc`
Missing keys are auto-registered with their default value and written back to this file on load.
## Global Settings
| Key | Type | Default | Description |
| ------------------------------ | ---------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `CommandPrefixes` | `string[]` | `["!"]` | Prefixes recognized for normal command execution. |
| `CommandSilentPrefixes` | `string[]` | `["/"]` | Prefixes recognized for silent command execution. |
| `AutoHotReload` | `boolean` | `true` | Enables automatic hot reload behavior. |
| `ProfilerLevel` | `number` | `0` | Profiler verbosity level. |
| `ConsoleFilter` | `boolean` | `true` | Enables or disables console filtering globally. |
| `PatchesToPerform` | `string[]` | `[]` | Patch identifiers to perform on startup. |
| `FollowServerGuidelines` | `boolean` | `true` | Applies server guideline behavior. Key name depends on the current game, e.g. `FollowCS2ServerGuidelines`. |
| `Language` | `string` | `"en"` | Default language code used by the server. |
| `UsePlayerLanguage` | `boolean` | `true` | Uses player language when available. |
| `ManualLoadPlugins` | `boolean` | `false` | Toggles manual plugin loading mode. |
| `PluginLoadOrder` | `string[]` | `[]` | Plugin load order list used by the core loader. |
| `DotnetCrashTracerLevel` | `number` | `0` | Crash tracer level. Supported values: `0`, `1`. |
| `WindowsFullDump` | `boolean` | `false` | Enables full memory dumps on crash (Windows only). |
## Unlocker Settings
`Unlocker` controls which restricted engine features are exposed.
| Key | Type | Default | Description |
| ------------- | --------- | ------- | ------------------------------------ |
| `Convars` | `boolean` | `false` | Unlocks restricted convars. |
| `ConCommands` | `boolean` | `false` | Unlocks restricted console commands. |
## Console Logger Settings
`ConsoleLogger` controls console output logging and log file rotation.
| Key | Type | Default | Description |
| ------------------------------- | ---------- | --------------------------------- | ---------------------------------------------------------------------------- |
| `Enable` | `boolean` | `true` | Enables console logging to file. |
| `ManagedEnable` | `boolean` | `true` | Enables console logging for managed (.NET) output. |
| `WriteIntervalMs` | `number` | `2000` | Interval, in milliseconds, between log file writes. |
| `Rotation.Enable` | `boolean` | `true` | Enables log file rotation. |
| `Rotation.Mode` | `string` | `"file_count"` | Rotation mode. Available: `"file_count"`, `"time_interval"`. |
| `Rotation.AvailableModes` | `string[]` | `["file_count", "time_interval"]` | Rotation modes recognized by the logger. |
| `Rotation.MaximumFiles` | `number` | `60` | Maximum number of rotated log files to keep (`file_count` mode). |
| `Rotation.DeleteOlderThanHours` | `number` | `168` | Deletes rotated log files older than this many hours (`time_interval` mode). |
## Menu Settings
`Menu` controls input mode, navigation behavior, and menu sounds.
| Key | Type | Default | Description |
| --------------------- | ---------- | -------------------- | -------------------------------------------------- |
| `AvailableInputModes` | `string[]` | `["button", "wasd"]` | Available menu input modes. |
| `InputMode` | `string` | `"button"` | Active menu input mode. |
| `NavigationPrefix` | `string` | `"➤"` | Prefix shown for the currently selected menu item. |
| `ItemsPerPage` | `number` | `5` | Number of menu items shown per page. |
### Menu Buttons
| Key | Default |
| ------------ | --------- |
| `Exit` | `"tab"` |
| `Scroll` | `"shift"` |
| `ScrollBack` | `"f"` |
| `Use` | `"e"` |
### Menu Sound
`Menu.Sound` defines the sound played for menu actions.
| Action | Name | Volume |
| -------- | ------------------- | ------ |
| `Exit` | `"Vote.Failed"` | `0.75` |
| `Scroll` | `"UI.ContractType"` | `0.75` |
| `Use` | `"Vote.Cast.Yes"` | `0.75` |
## SteamAuth Settings
`SteamAuth` controls Steam authentication mode.
| Key | Type | Default | Description |
| ---------------- | ---------- | ------------------------ | --------------------------- |
| `AvailableModes` | `string[]` | `["flexible", "strict"]` | Available Steam auth modes. |
| `Mode` | `string` | `"flexible"` | Active Steam auth mode. |