Skip to content

Plugin Development Tutorial

3cxc edited this page Sep 13, 2026 · 2 revisions

Plugin Development Tutorial

This tutorial walks you through creating a minimal plugin for Unknown: Site-0 using NativePluginKit.


1. Create the Project

dotnet new classlib -n HelloWorldPlugin -f net8.0
cd HelloWorldPlugin

Edit HelloWorldPlugin.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
    <NativeLib>Shared</NativeLib>
    <IlcExportUnmanagedEntrypoints>true</IlcExportUnmanagedEntrypoints>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\NativePluginKit\NativePluginKit.csproj" />
    <ProjectReference Include="..\NativePluginKit.SourceGenerators\NativePluginKit.SourceGenerators.csproj"
                      OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
    <UnmanagedEntryPointsAssembly Include="NativePluginKit" />
  </ItemGroup>
</Project>

2. Write the Plugin

Create HelloWorldPlugin.cs:

using NativePluginKit.Features.Logger;
using NativePluginKit.Loader.Plugins;

namespace HelloWorldPlugin;

public partial class HelloWorldPlugin : Plugin
{
    public override string Name        => "HelloWorldPlugin";
    public override string Description => "A simple hello world plugin.";
    public override string Author      => "Your Name";
    public override Version RequiredApiVersion => new(1, 0, 0);

    protected override void OnStart()
    {
        Log.PrintLog("Hello, World!");
    }

    public override void OnStop() { }
}

Note — The class must be declared partial. A source generator emits the required OnInit, OnStop export, and [ModuleInitializer] bootstrap for you.


3. Use Host APIs

Logging

Log.PrintLog("Something happened.");
Log.PrintWarning("A warning.");
Log.PrintError("An error.");
Log.Debug("Debug information.");

Scene switching

NativePluginKit.Features.FMOD.Transition.TransitionToScene(
    "res://scenes/menu.tscn", fadeOut: true);

Enumerating cards

using NativePluginKit.Features.Cards;

for (int i = 0; i < Card.GetCardCount(); i++)
{
    string name = Card.GetCardNameAt(i);
    Log.PrintLog($"{name} ({Card.GetCardTypeEnum(name)}) costs {Card.GetCost(name)}");
}

Enumerating entities

using NativePluginKit.Features.Entities;
using NativePluginKit.Features.Wrappers;

foreach (EntityId id in Entity.GetAllEntityIds())
{
    if (!Entity.Contains(id)) continue;
    Log.PrintLog($"{Entity.GetDisplayName(id)}: {Entity.GetCurrentHP(id)}/{Entity.GetMaxHP(id)}");
}

Subscribing to events

using NativePluginKit.Events;
using NativePluginKit.Features.Events;

EventBus.On<EntityDamagedEvent>(EventType.EntityDamaged, OnEntityDamaged);

private void OnEntityDamaged(EntityDamagedEvent e)
{
    var id = new EntityId(e.EntityId.Low, e.EntityId.High);
    Log.PrintLog($"{Entity.GetDisplayName(id)} took {e.HpDamage} damage");
}

Accessing the Raw API Table

HostApiTable api = Api;

4. Build & Deploy

dotnet publish -c Release -r win-x64

Copy bin/Release/net8.0/win-x64/publish/HelloWorldPlugin.dll into the host's <GameRoot>/plugins/ directory:

<GameRoot>/
├── ExampleClient.exe
└── plugins/
    └── HelloWorldPlugin.dll

Launch the client. You should see:

Loading plugin: HelloWorldPlugin v1.0.0.0 by Your Name
Hello, World!

5. Lifecycle

Stage Method Description
Load __GeneratedModuleInit [ModuleInitializer] — registers metadata before OnInit
Load __GeneratedOnInit Exported as OnInit — creates the instance and injects Api
Start OnStart() Business initialization
Stop OnStop() Release resources; host then frees the instance

6. Best Practices

  • Never throw from OnStart / OnStop — exceptions cannot cross the ABI boundary safely.
  • Keep the plugin self-contained — only depend on NativePluginKit and the BCL.
  • Match the API version — declare RequiredApiVersion.
  • Use Log.PrintLog — Console.WriteLine writes to the process stdout, which may not be visible in the client console.
  • Free all unmanaged resources in OnStop — including event subscriptions via EventBus.Off<T>.

Clone this wiki locally