-
-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Development Tutorial
3cxc edited this page Sep 13, 2026
·
2 revisions
This tutorial walks you through creating a minimal plugin for Unknown: Site-0 using NativePluginKit.
dotnet new classlib -n HelloWorldPlugin -f net8.0
cd HelloWorldPluginEdit 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>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 requiredOnInit,OnStopexport, and[ModuleInitializer]bootstrap for you.
Log.PrintLog("Something happened.");
Log.PrintWarning("A warning.");
Log.PrintError("An error.");
Log.Debug("Debug information.");NativePluginKit.Features.FMOD.Transition.TransitionToScene(
"res://scenes/menu.tscn", fadeOut: true);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)}");
}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)}");
}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");
}HostApiTable api = Api;dotnet publish -c Release -r win-x64Copy 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!
| 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 |
-
Never throw from
OnStart/OnStop— exceptions cannot cross the ABI boundary safely. -
Keep the plugin self-contained — only depend on
NativePluginKitand the BCL. -
Match the API version — declare
RequiredApiVersion. -
Use
Log.PrintLog—Console.WriteLinewrites to the process stdout, which may not be visible in the client console. -
Free all unmanaged resources in
OnStop— including event subscriptions viaEventBus.Off<T>.