Creating xPilot Plugins

Developer documentation only. This page is intended for developers who want to create xPilot client plugins for distribution. If you’re simply looking to install or use plugins, see the Install Plugins page instead.

xPilot plugins let developers extend the xPilot client with custom behavior for other xPilot users. Plugins are .NET assemblies loaded by the client at startup, and they can react to xPilot events, read the current connection and controller state, post debug messages to the client, request network actions, send text messages, and control a few simulator-facing features such as Mode C, ident, and PTT.

This page is for developers who want to create and distribute plugins that run inside the xPilot client. It covers the plugin interface, project setup, lifecycle, isolation and error handling, and the broker events, properties, and methods available to plugins.

Plugins run inside the xPilot client process. Keep plugin code lightweight, handle exceptions carefully, and avoid blocking event handlers.

xPilot client plugins are different from the X-Plane Resources/plugins/xPilot plugin. Client plugins are managed .NET assemblies installed into the xPilot application data Plugins folder. The plugin assembly itself is a .dll on Windows, macOS, and Linux.

Requirements

Create a C# class library that targets the same .NET version as the xPilot plugin interface. The current xPilot.PluginSdk interface assembly targets net10.0.

Your plugin DLL must contain at least one public, non-abstract class that implements Vatsim.Xpilot.PluginSdk.IPlugin and has a public parameterless constructor. xPilot initializes every compatible plugin type it finds in each plugin’s main DLL. See Plugin Layout for where files go.

Create the Project

Clone the plugin SDK project from the xPilot GitHub page, then create your plugin project next to it:

git clone https://github.com/xpilot-project/plugin-sdk.git
dotnet new classlib -n MyXpilotPlugin

For this example, place the projects next to each other like this:

PluginDevelopment/
  MyXpilotPlugin/
    MyXpilotPlugin.csproj
  xPilot.PluginSdk/
    xPilot.PluginSdk.csproj

Update your plugin project file to target net10.0 and reference the cloned xPilot.PluginSdk project:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="../xPilot.PluginSdk/xPilot.PluginSdk.csproj">
      <Private>false</Private>
    </ProjectReference>
  </ItemGroup>
</Project>

Set <Private>false</Private> on the project reference so your build does not copy xPilot.PluginSdk.dll into the plugin output folder. xPilot already provides that assembly at runtime and always uses its own copy. A copy shipped with your plugin is ignored, so leaving it out keeps your package smaller and avoids confusion.

Implement IPlugin

Implement IPlugin.Name and IPlugin.Initialize. Store the broker passed to Initialize if you need to call xPilot later, and subscribe to events from inside Initialize.

using System;
using Vatsim.Xpilot.PluginSdk;
using Vatsim.Xpilot.PluginSdk.Events;
using Vatsim.Xpilot.PluginSdk.Exceptions;

namespace MyXpilotPlugin;

public sealed class MyPlugin : IPlugin
{
    private IBroker? _broker;

    public string Name => "My xPilot Plugin";

    public void Initialize(IBroker broker)
    {
        // Bail out before subscribing if this xPilot is older than the SDK the plugin was built against.
        if (broker.ApiVersion < new Version(0, 2))
            return;

        _broker = broker;

        broker.NetworkConnected += OnNetworkConnected;
        broker.NetworkDisconnected += OnNetworkDisconnected;
        broker.PrivateMessageReceived += OnPrivateMessageReceived;
        broker.SessionEnded += OnSessionEnded;

        broker.PostDebugMessage($"{Name} loaded.");
    }

    private void OnNetworkConnected(object? sender, NetworkConnectedEventArgs e)
    {
        _broker?.PostDebugMessage($"Connected as {e.Callsign}.");

        try
        {
            _broker?.RequestMetar("KJFK");
        }
        catch (NotConnectedException)
        {
            // The connection closed before the request was sent.
        }
    }

    private void OnNetworkDisconnected(object? sender, EventArgs e)
    {
        _broker?.PostDebugMessage("Disconnected from VATSIM.");
    }

    private void OnPrivateMessageReceived(object? sender, PrivateMessageReceivedEventArgs e)
    {
        _broker?.PostDebugMessage($"Private message from {e.From}: {e.Message}");
    }

    private void OnSessionEnded(object? sender, EventArgs e)
    {
        if (_broker == null)
            return;

        _broker.NetworkConnected -= OnNetworkConnected;
        _broker.NetworkDisconnected -= OnNetworkDisconnected;
        _broker.PrivateMessageReceived -= OnPrivateMessageReceived;
        _broker.SessionEnded -= OnSessionEnded;
    }
}

Compatibility

The SDK is in beta, so any build may include breaking changes. The xPilot.PluginSdk assembly version is bumped when the SDK changes in a breaking way, and IBroker.ApiVersion reports the SDK version the running xPilot was built with.

xPilot does not refuse to load a plugin built against a different SDK version, and plugins have no manifest or attribute for declaring a minimum version. Instead, check ApiVersion at the top of Initialize and return before subscribing to anything if it isn’t a version you support.

  • A plugin built against a newer SDK still loads if it only uses APIs that the running xPilot has.
  • A plugin that calls a member the running xPilot lacks fails inside Initialize with a MissingMethodException. xPilot logs the error and other plugins are unaffected.
  • A version of xPilot older than the one that introduced ApiVersion doesn’t have the property at all, so wrap that first call in a try/catch if you support older builds.

Plugin Lifecycle

  • xPilot discovers and initializes plugins once, at startup, after the main window is shown. Initialize is called once per plugin type per process. Plugins are not reloaded while xPilot is running, so restart xPilot to load, update, or remove a plugin.
  • Plugins are initialized before a network connection can exist. NetworkConnected is normally the first connection signal a plugin sees.
  • SessionEnded is the only shutdown signal. It fires once, when xPilot exits, and never on a network disconnect. There is no separate dispose or unload callback, so unsubscribe from events, stop threads and timers, and release resources there.
  • A plugin publishes its own version through IPlugin.Name. xPilot logs Plugin loaded: {Name} ({Path}), so include your version in Name.

Threading and Event Delivery

  • Broker events are raised on the UI thread, in the order xPilot processes them, and never on the thread that produced them. Handlers must return quickly. If you need to do real work, copy the event into a bounded in-memory, file, or IPC queue and process it on your own thread.
  • Events are delivered in order within a connection. There is no deduplication or replay, and messages received before a plugin was initialized are never replayed. Treat plugin startup, and each reconnect, as a new observation epoch.
  • On disconnect, xPilot clears its controller and aircraft tracking before it raises NetworkDisconnected, so events after a reconnect describe the new connection.

Error Handling and Isolation

xPilot isolates plugin failures so one plugin can’t take down the client or other plugins.

Failure What happens
Exception in a plugin’s constructor or Initialize Caught and logged as an error. xPilot and other plugins continue.
Plugin type with no public parameterless constructor Skipped and logged as an error.
A .dll that isn’t a valid .NET assembly Skipped and logged as an error.
Exception in an event handler Caught per handler and logged as an error naming the event, the plugin assembly, and the handler. Other handlers for the same event, other plugins, and later events are unaffected.

Plugins are isolated from each other’s assemblies, not from the process. Each plugin loads in its own assembly load context, so it can use its own dependencies, including different versions of the same library that another plugin uses. The only assembly shared with xPilot is xPilot.PluginSdk, which always comes from xPilot.

Plugins still run inside the xPilot process and are not sandboxed. A plugin that blocks the UI thread, throws on its own threads, or uses a lot of memory or CPU can still affect xPilot. Libraries that keep static state, such as a logging library’s global logger, get their own copy inside the plugin, so a plugin’s log output does not appear in xPilot’s log file.

Broker Events

The IBroker interface exposes these events:

Event Use
SessionEnded xPilot is closing. Unsubscribe from events and stop background work here. It is the only cleanup callback.
NetworkConnected / NetworkDisconnected The VATSIM network session changed state.
PrivateMessageReceived / PrivateMessageSent Private text messages were received or sent.
RadioMessageReceived / RadioMessageSent Text radio messages were received or sent.
ServerMessageReceived The network server itself sent a text message.
BroadcastMessageReceived A network broadcast was received.
MetarReceived A requested METAR was received.
AtisReceived Requested controller information or text ATIS was received.
ControllerAdded / ControllerDeleted A controller appeared or disappeared.
ControllerFrequencyChanged A controller changed primary frequency.
ControllerLocationChanged A controller position changed.
SelcalAlertReceived A SELCAL alert was received.
AircraftAdded / AircraftUpdated / AircraftDeleted Nearby aircraft were added, updated, or removed from the simulator session.

Radio frequencies exposed by message events are expressed in Hz as integers. For example, 119.950 is represented as 119950000.

Event Details

Private messages. PrivateMessageReceived provides only From and Message. xPilot does not classify private messages, so automated messages, controller PDC/ACARS-style messages, and ordinary chat are all delivered the same way. From is the network sender callsign exactly as received, and Message is delivered verbatim, including line breaks, spacing, and Unicode. xPilot does not truncate it. The SDK defines no maximum length, and practical limits come from the network protocol. Two cases are not delivered through this event: text messages from the network server itself, which are delivered through ServerMessageReceived instead, and any private message received while connected in tower view mode, where xPilot sends an automatic reply instead.

Server messages. ServerMessageReceived provides only Message, the text the network server sent, delivered verbatim. These are network notices, not messages from another user, and they are also shown in the xPilot message area as system notices.

Network state. NetworkConnectedEventArgs.Callsign is the callsign of the active connection, which is the identity the network uses to route private messages.

Controllers. The controller events come from the same network messages that populate xPilot’s controller list, and xPilot applies no range or relevance filter. ControllerAdded fires the first time xPilot sees a controller during a connection, and it is not replayed to handlers that subscribe later. Frequencies are int Hz (for example 123725000 is 123.725 MHz) and latitude and longitude are double degrees as reported by the network. The SDK does not distinguish an unknown or defaulted value from a real one, so 0 can appear and can’t be told apart from valid data. ControllerFrequencyChanged fires when the primary frequency changes, and ControllerLocationChanged fires when the position moves by more than about 1e-9 degrees. The SDK exposes callsign, frequency, and position only. It does not expose station aliases, controller type or role, range, visibility, or a relevance result.

Broker Properties

Plugins can read this state through IBroker at any time:

Property Use
ApiVersion The SDK version the running xPilot was built with. See Compatibility.
IsConnected Whether xPilot is currently connected to the network.
Callsign The callsign of the current connection, or null when not connected.

Use IsConnected and Callsign to resynchronize when your plugin’s own consumer starts late or restarts. Events only report transitions and are not replayed.

Broker Methods

Plugins can call these methods through IBroker:

Method Use
RequestConnect(callsign, typeCode, selcalCode) Connect as a pilot.
RequestConnectAsObserver(callsign) Connect as an observer.
RequestConnectAsTowerView() Connect in tower view mode.
RequestDisconnect() Disconnect from the network.
RequestMetar(station) Request a METAR.
RequestAtis(callsign) Request controller information or text ATIS.
SendPrivateMessage(to, message) Send a private message.
SendRadioMessage(message) Send a text radio message on the current transmit radio or radios.
PostDebugMessage(message) Write a debug message to the xPilot message area.
SetModeC(modeC) Toggle Mode C.
SquawkIdent() Trigger ident.
SetPtt(pressed) Set PTT pressed or released.
GetControllers() Get a snapshot of the controllers xPilot currently tracks. See below.

GetControllers() returns a copy of the controller set xPilot tracks for the current connection as a list of ControllerInfo objects, each with Callsign, Frequency (Hz), Latitude, and Longitude. It is safe to call from any thread, returns an empty list when disconnected, and has no effect on later events. Use it when you need the current set at a specific moment, such as rebuilding state after a queue overflowed or a consumer restarted.

Some methods throw state exceptions. Handle AlreadyConnectedException, NotConnectedException, SimNotReadyException, and other relevant exceptions where appropriate, because client state can change at any time.

Build the Plugin

For distribution, publish a release build:

dotnet publish -c Release

Use the output from bin/Release/net10.0/publish/. Include your plugin DLL and any dependency DLLs your plugin requires. You don’t need to include xPilot.PluginSdk.dll, and if you do, xPilot ignores it and uses its own copy.

Managed .NET plugins only need to be compiled once. The same plugin DLL can run on Windows, macOS, and Linux as long as the plugin and its dependencies are platform-independent. It is still recommended to test your plugin on all three xPilot platforms before publishing it.

Plugin Layout

Give each plugin its own folder inside the xPilot Plugins folder, with the main plugin DLL named after the folder and its dependencies beside it:

Plugins/
  MyXpilotPlugin/
    MyXpilotPlugin.dll        <- main plugin assembly, scanned for IPlugin types
    SomeDependency.dll        <- dependencies are loaded for this plugin only
  • The main DLL must have the same name as its folder. Only that DLL is scanned for IPlugin types. Dependency DLLs beside it are not treated as plugins.
  • Other subfolder layouts, and files that don’t end in .dll, are not scanned.
  • DLLs placed loose in the Plugins folder are still loaded, but the per-plugin folder is the preferred layout because each plugin’s dependencies stay together and separate.
  • There is no manifest, signing step, registration, or allow-list.

Install the Plugin

For end-user installation steps, see Installing Plugins.

Open the xPilot plugins folder with the .plugins command in the xPilot command line. You can also create the folder manually if it doesn’t exist:

Platform Plugins folder
Windows %LOCALAPPDATA%\org.vatsim.xpilot\Plugins
macOS ~/Library/Application Support/org.vatsim.xpilot/Plugins
Linux ~/.local/share/org.vatsim.xpilot/Plugins

Copy your plugin folder, with its DLL and any dependency DLLs or native libraries, into that folder. Close xPilot before copying, because plugin files can be locked while xPilot is running. Restart xPilot afterwards. xPilot loads plugins only during startup, and plugins can’t be reloaded while it is running. An installer should never overwrite or remove other plugins’ folders.

If the plugin does not load, check the xPilot logs with the .logs command. Loading and initialization failures are logged as errors with the full exception, and each successfully loaded plugin is logged as Plugin loaded: {Name} ({Path}). Include your plugin version in IPlugin.Name so it appears in support logs.


This site uses Just the Docs, a documentation theme for Jekyll.