Skip to content

C# Script Plugin Development Guide

Amos edited this page Jan 16, 2026 · 6 revisions

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.

Prerequisites

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.


Why C# Script Plugins?

Feature C# Script Plugins JINT (JavaScript)
IntelliSense ✅ Full IDE support ❌ Limited
Type Safety ✅ Compile-time errors ❌ Runtime only
Performance ✅ Native .NET speed ⚠️ Interpreted
Hot Reload ✅ Save & auto-reload ✅ Supported
Debugging ✅ Full .NET debugging ⚠️ Limited
All .NET APIs ✅ Full access ⚠️ Subset
Dependency Injection ✅ Native support ⚠️ Via 'Service Locator' Subsystem

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.


Quick Start

  1. Create a .cs file in your Plugins/ folder (e.g., MyPlugin.cs)
  2. Add the package reference at the top for IntelliSense
  3. Implement IPluginV2 interface
  4. Save — the plugin automatically compiles and loads!

Minimal Example

#: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 (New in .NET 10)

The #:package directive is a new .NET 10 feature for file-based applications that enables NuGet package references directly in C# source files.

Syntax

#:package PackageName@Version

How It Works

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

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

Example

#: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 APIs

Tip

Check NuGet for the latest version of RaidMax.IW4MAdmin.SharedLibraryCore to use in your directive.


Plugin Structure

The IPluginV2 Interface

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; }
}

Full Plugin Template

#: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!");
    }
}

Dependency Injection & Configuration

RegisterDependencies Method

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

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
}

Available Services for Injection

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.


Event System

IW4MAdmin uses static events for decoupled, type-safe event handling. Events are grouped into three interfaces:

Event Interfaces

Interface Purpose
IManagementEventSubscriptions Client lifecycle, penalties, commands, auth
IGameEventSubscriptions In-game events (kills, messages, match events)
IGameServerEventSubscriptions Server monitoring events

Subscribing to 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;
    }
}

Available Events

IManagementEventSubscriptions

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

IGameEventSubscriptions

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

IGameServerEventSubscriptions

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

Creating Commands

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;
    }
}

Command with Target

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 Levels

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

Hot Reload

C# script plugins support instant hot reload:

  1. Edit your .cs file in any editor
  2. Save the file
  3. The plugin automatically:
    • Unloads the old version (calling Dispose())
    • Recompiles the new code
    • Loads the new version
    • Re-registers commands

Caution

Always implement Dispose() to unsubscribe from events. Failing to do so will cause event handlers to run multiple times after reload!


Color Codes

IW4MAdmin supports two color syntax options for player messages:

Named Colors (Recommended)

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);

Legacy Color Codes

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);

Complete Example

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

Troubleshooting

Compilation Errors

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

Plugin Not Loading

  1. Ensure the file is in the Plugins/ folder (not a subdirectory)
  2. Verify the plugin implements IPluginV2
  3. Check that the #:package version matches your IW4MAdmin version

Events Firing Multiple Times

You forgot to unsubscribe in Dispose(). Always match every += with a corresponding -=.

IntelliSense Not Working

  1. Ensure you have the #:package directive at the very top of the file
  2. Trust the workspace — VS Code prompts you to trust the folder; this is required for NuGet restore
  3. Ensure the .NET 10 SDK is installed and in your PATH
  4. Wait for C# Dev Kit to restore packages (check the Output panel → C# Dev Kit)
  5. Try restarting VS Code or reloading the window (Ctrl+Shift+P → "Reload Window")

Additional Resources

Clone this wiki locally