LANCommander/LANCommander.Documentation/Plugins/Getting Started.md
2026-07-28 19:44:59 -05:00

6.2 KiB

sidebar_label sidebar_position
Getting Started 2

Getting Started

This guide walks through building a minimal plugin from an empty project to a working drop-in that the launcher loads at startup. If you'd rather read finished code, the repository ships a complete reference plugin under LANCommander.SamplePlugin that exercises every extension point described here.

Prerequisites

  • The .NET 10 SDK.
  • A local checkout of the LANCommander source, or a package/binary reference to the contract assemblies listed below. The framework binds plugins against the host's already-loaded assemblies, so you build against the same contract assemblies the host ships.

1. Create a class library

A plugin is an ordinary class library targeting net10.0:

dotnet new classlib -n MyCompany.MyPlugin -f net10.0

2. Reference the contract assemblies

Reference the LANCommander assemblies that expose the extension points you need. Reference them with Private=false so your plugin binds against the host's already-loaded copies at runtime rather than shipping (and loading) its own duplicates:

<ItemGroup>
  <!-- Core plugin contracts, events, and the PowerShell extension point. -->
  <ProjectReference Include="..\LANCommander.SDK\LANCommander.SDK.csproj" Private="false" />

  <!-- Launcher UI extension points (only needed if you extend the launcher UI). -->
  <ProjectReference Include="..\LANCommander.Launcher.Plugins\LANCommander.Launcher.Plugins.csproj" Private="false" />

  <!-- Server contracts such as IMetadataProvider (only needed for server extensions). -->
  <ProjectReference Include="..\LANCommander.Server.Services\LANCommander.Server.Services.csproj" Private="false" />
</ItemGroup>

:::note Private=false keeps the contract assemblies out of your plugin's output folder. This is important: the framework preserves type identity across the plugin's load context by deferring these shared assemblies to the host. Your plugin's own private dependencies (NuGet packages, helper libraries) should ship normally so they land next to your plugin's DLL. :::

3. Implement the entry point

Every plugin has a single entry point that implements IPlugin. The two lifecycle methods map directly onto the host's "build the DI container once" model:

  • ConfigureServices runs while the host is still populating its service collection, before the provider is built. Only register services here — do not resolve them.
  • InitializeAsync runs after the provider is built. Resolve services, subscribe to events, and do any asynchronous startup work here. You receive a PluginContext with the host identity, a scoped service provider, the plugin's directory, and a scoped logger.
using LANCommander.SDK.Plugins;
using LANCommander.SDK.Plugins.Events;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace MyCompany.MyPlugin;

public sealed class MyPlugin : IPlugin
{
    public string Id => "com.mycompany.myplugin";
    public string Name => "My Plugin";
    public string Version => "1.0.0";
    public string Author => "My Company";

    private IDisposable? _launchSubscription;

    public void ConfigureServices(IServiceCollection services)
    {
        // Register anything you'll resolve later, or any extension point implementations.
        // e.g. services.AddSingleton<ISettingsPageExtension, MySettingsExtension>();
    }

    public Task InitializeAsync(PluginContext context, CancellationToken cancellationToken)
    {
        var events = context.Services.GetRequiredService<IPluginEventBus>();

        _launchSubscription = events.Subscribe<GameBeforeLaunchEvent>((evt, ct) =>
        {
            context.Logger.LogInformation("Game {GameId} is about to launch", evt.GameId);
            return Task.CompletedTask;
        });

        context.Logger.LogInformation("{Name} initialized on host {Host}", Name, context.Host);

        return Task.CompletedTask;
    }
}

4. Mark the assembly as a plugin

Discovery is driven by an assembly-level [LANCommanderPlugin] attribute. It names the entry point and declares compatibility metadata. Place it anywhere in your project (a common choice is above the namespace declaration in your entry point file):

using LANCommander.SDK.Plugins;

[assembly: LANCommanderPlugin(
    typeof(MyCompany.MyPlugin.MyPlugin),
    Id = "com.mycompany.myplugin",
    MinHostVersion = "1.1.0",
    Hosts = PluginHost.Server | PluginHost.Launcher)]
  • Id is optional; when omitted the loader falls back to the entry point's IPlugin.Id.
  • MinHostVersion / MaxHostVersion are optional SemVer bounds (inclusive). A plugin outside the host's version range is skipped.
  • Hosts declares which hosts the plugin supports. Defaults to both server and launcher.

5. Build and deploy

Build your plugin and copy its output into a subfolder of LANCommander's Plugins directory. That directory lives inside LANCommander's data folder — Data/Plugins next to the executable, or under your user profile's application data if the install directory is not writable:

Data/
└── Plugins/
    └── MyCompany.MyPlugin/
        ├── MyCompany.MyPlugin.dll
        ├── MyCompany.MyPlugin.deps.json
        └── (your private dependencies)

The loader prefers an assembly named after the folder (MyCompany.MyPlugin.dll in the example above), so naming the folder after your main assembly is the most reliable convention.

6. Verify it loaded

Start the host and check the logs. A successful load emits an entry like:

Loaded plugin 'My Plugin' (com.mycompany.myplugin) v1.0.0 by My Company

If your plugin does not appear, the logs will explain why — a missing [LANCommanderPlugin] attribute, an incompatible host version, a host it does not target, or an exception thrown during ConfigureServices. Every failure is isolated and logged rather than crashing the host.

Next steps

Now that your plugin loads, head to Extension Points to add real functionality, or browse the API Reference for the complete surface.