-
-
Notifications
You must be signed in to change notification settings - Fork 50
C# Script Plugin Development Guide
Important
This feature requires IW4MAdmin 2026.1+ built with .NET 10 or later.
IW4MAdmin supports loading .cs script files directly from the Plugins/ folder with full IntelliSense support, hot reload, and automatic dependency injection — no separate project file or compilation step required.
- .NET 10 SDK or later — Download here
- VS Code with the C# Dev Kit extension
Note
C# Script plugins are designed for rapid development without requiring a full IDE or project compilation. If you're using Visual Studio and are comfortable with compiled plugins, you may prefer the traditional Plugin Development approach instead.
| Feature | C# Script Plugins | JINT (JavaScript) |
|---|---|---|
| IntelliSense | ✅ Full IDE support | ❌ Limited |
| Type Safety | ✅ Compile-time errors | ❌ Runtime only |
| Performance | ✅ Native .NET speed | |
| Hot Reload | ✅ Save & auto-reload | ✅ Supported |
| Debugging | ✅ Full .NET debugging | |
| All .NET APIs | ✅ Full access | |
| Dependency Injection | ✅ Native support |
C# script plugins provide rapid development with the full power of .NET, making them ideal for simple or complex plugins that need access to databases, logging, configuration, and the complete IW4MAdmin API.
-
Create a
.csfile in yourPlugins/folder (e.g.,MyPlugin.cs) - Add the package reference at the top for IntelliSense
-
Implement
IPluginV2interface - Save — the plugin automatically compiles and loads!
#:package RaidMax.IW4MAdmin.SharedLibraryCore@2026.1.6.1
using SharedLibraryCore.Interfaces;
public class MyPlugin : IPluginV2
{
public string Name => "My Plugin";
public string Author => "Your Name";
public string Version => "1.0";
}The #:package directive is a new .NET 10 feature for file-based applications that enables NuGet package references directly in C# source files.
#:package PackageName@Version-
For IntelliSense: VS Code with C# Dev Kit recognizes this directive and automatically restores the NuGet package, enabling full code completion, type checking, and navigation.
-
At Runtime: IW4MAdmin's compiler comments out the directive during compilation since the referenced assemblies (
SharedLibraryCore, etc.) are already loaded into the runtime.
Tip
First time setup: When you open a .cs script file in VS Code, you may need to Trust the workspace when prompted. This allows C# Dev Kit to restore NuGet packages and enable IntelliSense.
#:package RaidMax.IW4MAdmin.SharedLibraryCore@2026.1.6.1
// After this line, you get full IntelliSense for:
// - SharedLibraryCore.Interfaces.IPluginV2
// - SharedLibraryCore.Commands.Command
// - SharedLibraryCore.Events.*
// - Data.Models.*
// - And all other IW4MAdmin APIsTip
Check NuGet for the latest version of RaidMax.IW4MAdmin.SharedLibraryCore to use in your directive.
Every C# script plugin must implement IPluginV2:
public interface IPluginV2 : IModularAssembly, IDisposable
{
// Optional: Register custom services/configuration
static void RegisterDependencies(IServiceCollection serviceCollection) { }
// Optional: Cleanup when plugin unloads (IMPORTANT for event subscriptions!)
void IDisposable.Dispose() { }
}
public interface IModularAssembly
{
string Name { get; }
string Author { get; }
string Version { get; }
}#:package RaidMax.IW4MAdmin.SharedLibraryCore@2026.1.6.1
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.Interfaces.Events;
public class MyPlugin : IPluginV2
{
public string Name => "My Plugin";
public string Author => "Your Name";
public string Version => "1.0";
private readonly ILogger<MyPlugin> _logger;
// Register dependencies before plugin instantiation
public static void RegisterDependencies(IServiceCollection serviceCollection)
{
// Register configuration, custom services, etc.
}
// Constructor with dependency injection
public MyPlugin(ILogger<MyPlugin> logger)
{
_logger = logger;
// Subscribe to events
IManagementEventSubscriptions.ClientStateAuthorized += OnPlayerJoined;
_logger.LogInformation("My Plugin loaded!");
}
private Task OnPlayerJoined(ClientStateAuthorizeEvent e, CancellationToken token)
{
e.Client.Tell("Welcome to the server!");
return Task.CompletedTask;
}
// CRITICAL: Unsubscribe from events to prevent memory leaks!
public void Dispose()
{
IManagementEventSubscriptions.ClientStateAuthorized -= OnPlayerJoined;
_logger.LogInformation("My Plugin unloaded!");
}
}The static RegisterDependencies method is called before your plugin is instantiated, allowing you to register services that can be injected into your plugin constructor.
public static void RegisterDependencies(IServiceCollection serviceCollection)
{
// Register a configuration class
serviceCollection.AddConfiguration<MyPluginConfiguration>(
"MyPluginSettings", // Creates Configuration/MyPluginSettings.json
new MyPluginConfiguration()); // Default values
// Register custom services
serviceCollection.AddSingleton<IMyCustomService, MyCustomService>();
}Configuration classes are automatically serialized to JSON and saved in the Configuration/ folder:
public class MyPluginConfiguration
{
public bool EnableFeature { get; set; } = true;
public string WelcomeMessage { get; set; } = "Welcome!";
public int MaxItems { get; set; } = 10;
}The configuration file (Configuration/MyPluginSettings.json) is automatically created on first run:
{
"EnableFeature": true,
"WelcomeMessage": "Welcome!",
"MaxItems": 10
}IW4MAdmin exposes many services for dependency injection. Common services include logging (ILogger<T>), the core manager (IManager), localization (ITranslationLookup), metadata storage (IMetaServiceV2), and database access (IDatabaseContextFactory).
Tip
Discovering available services: In VS Code, use F12 (Go to Definition) on any interface like IManager to inspect (decompile) the package and explore all available APIs. You can also browse the SharedLibraryCore source for the full API surface.
IW4MAdmin uses static events for decoupled, type-safe event handling. Events are grouped into three interfaces:
| Interface | Purpose |
|---|---|
IManagementEventSubscriptions |
Client lifecycle, penalties, commands, auth |
IGameEventSubscriptions |
In-game events (kills, messages, match events) |
IGameServerEventSubscriptions |
Server monitoring events |
using SharedLibraryCore.Interfaces.Events;
using SharedLibraryCore.Events.Management;
using SharedLibraryCore.Events.Game;
public class EventExamplePlugin : IPluginV2
{
public string Name => "Event Example";
public string Author => "Developer";
public string Version => "1.0";
public EventExamplePlugin()
{
// Management Events
IManagementEventSubscriptions.ClientStateAuthorized += OnClientAuthorized;
IManagementEventSubscriptions.ClientPenaltyAdministered += OnPenalty;
// Game Events
IGameEventSubscriptions.ClientKilled += OnKill;
IGameEventSubscriptions.ClientMessaged += OnMessage;
// Server Events
IGameServerEventSubscriptions.MonitoringStarted += OnServerStart;
}
private Task OnClientAuthorized(ClientStateAuthorizeEvent e, CancellationToken token)
{
// e.Client - The player who joined
return Task.CompletedTask;
}
private Task OnPenalty(ClientPenaltyEvent e, CancellationToken token)
{
// e.Client - Penalized player
// e.Penalty - Penalty details
return Task.CompletedTask;
}
private Task OnKill(ClientKillEvent e, CancellationToken token)
{
// e.Attacker, e.Victim, e.Weapon, e.HitLocation, etc.
return Task.CompletedTask;
}
private Task OnMessage(ClientMessageEvent e, CancellationToken token)
{
// e.Origin - Player who sent message
// e.Message - The chat message
return Task.CompletedTask;
}
private Task OnServerStart(MonitorStartEvent e, CancellationToken token)
{
// e.Server - The server that started monitoring
return Task.CompletedTask;
}
public void Dispose()
{
// ALWAYS unsubscribe!
IManagementEventSubscriptions.ClientStateAuthorized -= OnClientAuthorized;
IManagementEventSubscriptions.ClientPenaltyAdministered -= OnPenalty;
IGameEventSubscriptions.ClientKilled -= OnKill;
IGameEventSubscriptions.ClientMessaged -= OnMessage;
IGameServerEventSubscriptions.MonitoringStarted -= OnServerStart;
}
}| Event | Description |
|---|---|
Load |
Manager is loading |
Unload |
Manager is restarting/shutting down |
ClientStateInitialized |
Client enters tracked state |
ClientStateAuthorized |
Client fully authorized to play |
ClientStateDisposed |
Client left/disconnected |
ClientPenaltyAdministered |
Penalty applied (ban, kick, etc.) |
ClientPenaltyRevoked |
Penalty removed (unban, unflag) |
ClientCommandExecuted |
After a command completes |
ClientPermissionChanged |
Player permission level changed |
ClientLoggedIn |
Player logged in (webfront/ingame) |
ClientLoggedOut |
Player logged out |
ClientPersistentIdReceived |
Stats file marker received |
| Event | Description |
|---|---|
MatchStarted |
Game match started (InitGame) |
MatchEnded |
Game match ended (ShutdownGame) |
ClientEnteredMatch |
Player joined match (J;) |
ClientExitedMatch |
Player left match (Q;) |
ClientJoinedTeam |
Player changed team (JT;) |
ClientDamaged |
Player took damage (D;) |
ClientKilled |
Player was killed (K;) |
ClientMessaged |
Chat message sent (say;) |
ClientEnteredCommand |
Command entered (!command) |
ScriptEventTriggered |
Custom GSE event (GSE;) |
GameLogEventTriggered |
Unhandled game log line |
| Event | Description |
|---|---|
ServerAdded |
Server added at runtime |
ServerRemoved |
Server removed at runtime |
MonitoringStarted |
IW4MAdmin started monitoring server |
MonitoringStopped |
IW4MAdmin stopped monitoring server |
ConnectionInterrupted |
Lost connection to server |
ConnectionRestored |
Connection restored to server |
ClientDataUpdated |
Received updated client data |
ServerCommandExecuted |
RCON command executed |
ServerValueRequested |
Dvar value requested |
ServerValueReceived |
Dvar value received |
ServerStatusReceived |
Server status response received |
Commands are automatically discovered from your plugin assembly. Simply extend the Command class:
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using Data.Models.Client;
public class GreetCommand : Command
{
public GreetCommand(CommandConfiguration config, ITranslationLookup lookup)
: base(config, lookup)
{
Name = "greet";
Description = "Send a friendly greeting";
Alias = "hi";
Permission = EFClient.Permission.User;
RequiresTarget = false;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
gameEvent.Origin.Tell($"^2Hello, ^7{gameEvent.Origin.Name}^2!");
return Task.CompletedTask;
}
}public class HugCommand : Command
{
public HugCommand(CommandConfiguration config, ITranslationLookup lookup)
: base(config, lookup)
{
Name = "hug";
Description = "Hug another player";
Alias = "embrace";
Permission = EFClient.Permission.User;
RequiresTarget = true;
}
public override Task ExecuteAsync(GameEvent gameEvent)
{
var hugger = gameEvent.Origin;
var target = gameEvent.Target;
gameEvent.Owner.Broadcast($"^5{hugger.Name} ^7hugged ^5{target.Name}^7!");
return Task.CompletedTask;
}
}| Permission | Level | Description |
|---|---|---|
User |
0 | All players |
Trusted |
1 | Trusted players |
Moderator |
2 | Moderators |
Administrator |
3 | Administrators |
SeniorAdmin |
4 | Senior Admins |
Owner |
5 | Server Owner |
Console |
6 | Console only |
C# script plugins support instant hot reload:
-
Edit your
.csfile in any editor - Save the file
- The plugin automatically:
- Unloads the old version (calling
Dispose()) - Recompiles the new code
- Loads the new version
- Re-registers commands
- Unloads the old version (calling
Caution
Always implement Dispose() to unsubscribe from events. Failing to do so will cause event handlers to run multiple times after reload!
IW4MAdmin supports two color syntax options for player messages:
Use the (Color::Name) syntax for readable, platform-independent colors:
| Syntax | Color |
|---|---|
(Color::Black) |
Black |
(Color::Red) |
Red |
(Color::Green) |
Green |
(Color::Yellow) |
Yellow |
(Color::Blue) |
Blue |
(Color::Cyan) |
Cyan |
(Color::Purple) |
Purple |
(Color::Pink) |
Pink |
(Color::White) |
White |
(Color::Grey) |
Grey |
(Color::Accent) |
Theme accent |
player.Tell("(Color::Green)Success! (Color::White)Your score: (Color::Yellow)" + score);Traditional caret-based codes are also supported:
| Code | Color |
|---|---|
^0 |
Black |
^1 |
Red |
^2 |
Green |
^3 |
Yellow |
^4 |
Blue |
^5 |
Cyan |
^6 |
Pink |
^7 |
White |
player.Tell("^2Success! ^7Your score: ^3" + score);See SimpleWelcome Plugin for a complete, documented example that demonstrates:
- Plugin structure with
IPluginV2 - Configuration with
RegisterDependencies - Event subscription and cleanup
- Custom commands
- Logging
- Player messaging
Check the IW4MAdmin logs for detailed error messages with line numbers:
[ERROR] Compilation failed for MyPlugin:
[15:42] CS1002: ; expected
[23:8] CS0246: The type or namespace name 'XYZ' could not be found
- Ensure the file is in the
Plugins/folder (not a subdirectory) - Verify the plugin implements
IPluginV2 - Check that the
#:packageversion matches your IW4MAdmin version
You forgot to unsubscribe in Dispose(). Always match every += with a corresponding -=.
- Ensure you have the
#:packagedirective at the very top of the file - Trust the workspace — VS Code prompts you to trust the folder; this is required for NuGet restore
- Ensure the .NET 10 SDK is installed and in your PATH
- Wait for C# Dev Kit to restore packages (check the Output panel → C# Dev Kit)
- Try restarting VS Code or reloading the window (
Ctrl+Shift+P→ "Reload Window")