C# Plugin System
Moongate supports startup-loaded C# plugins from the runtime plugins/<plugin-id>/ directory.
If your local runtime root is the repository moongate_data/ folder, the effective path becomes
moongate_data/plugins/<plugin-id>/.
This system is intentionally simple:
- plugins are loaded only at startup
- there is no unload or hot reload
- dependencies are resolved by plugin id
- plugins can register runtime contributions during bootstrap
- plugins can run lightweight initialization after the server is ready
If you want the shortest path from zero to a working plugin project, start with Create Your First Plugin. If you specifically want a compiled admin/operator command after that, continue with Create Your First C# Admin Command.
Plugin Folder Layout
Each plugin lives in its own directory:
plugins/my-plugin/
manifest.json
bin/
MyPlugin.dll
data/
scripts/
assets/
Only manifest.json and the configured entry assembly are required.
Authoring a Plugin Project
The minimum plugin project usually references:
Moongate.Plugin.AbstractionsMoongate.Server.Abstractions
If the plugin registers custom persisted entities or works directly with runtime entities, it will typically also reference:
Moongate.PersistenceMoongate.UO.Data
When consuming Moongate from NuGet, use package references instead of a project reference to
Moongate.Server.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moongate.Plugin.Abstractions" Version="0.34.0" />
<PackageReference Include="Moongate.Server.Abstractions" Version="0.34.0" />
</ItemGroup>
</Project>
If you are developing the plugin inside the Moongate repository, local project references are still fine for day-to-day iteration:
<ItemGroup>
<ProjectReference Include="..\..\src\Moongate.Plugin.Abstractions\Moongate.Plugin.Abstractions.csproj" />
<ProjectReference Include="..\..\src\Moongate.Server.Abstractions\Moongate.Server.Abstractions.csproj" />
</ItemGroup>
Plugin Template
Moongate also ships a dotnet new template package for plugin authors.
Install the template:
dotnet new install Moongate.Templates::<version>
Create a plugin:
dotnet new moongate-plugin --name MyPlugin --pluginId my-plugin --authors "Squid" --description "Example plugin"
Create a plugin with persistence references:
dotnet new moongate-plugin --name MyPlugin --pluginId my-plugin --authors "Squid" --description "Example plugin" --withPersistence true
The generated project includes:
- a minimal
IMoongatePluginimplementation - a
manifest.jsonaligned with the generated entry assembly and entry type - starter
data/,scripts/, andassets/folders - packaging scripts that assemble a runtime-ready plugin directory and zip archive
Package the generated plugin:
bash scripts/pack-plugin.sh
PowerShell:
pwsh ./scripts/pack-plugin.ps1
The packaging scripts produce:
artifacts/<plugin-id>/artifacts/<plugin-id>.zip
NuGet Packages
The plugin SDK is intentionally split into a small set of publishable packages:
Moongate.Plugin.AbstractionsMoongate.Server.AbstractionsMoongate.PersistenceMoongate.UO.DataMoongate.Templates
These packages depend on the shared Moongate runtime libraries that are published alongside them with the same version:
Moongate.CoreMoongate.AbstractionsMoongate.NetworkMoongate.Network.Packets
Use the same package version across the whole Moongate dependency chain.
Recommended publish order:
Moongate.CoreMoongate.AbstractionsMoongate.NetworkMoongate.UO.DataMoongate.Network.PacketsMoongate.PersistenceMoongate.Plugin.AbstractionsMoongate.Server.AbstractionsMoongate.Templates
After build, copy the plugin output to the runtime plugin folder so the final layout matches the manifest:
plugins/my-plugin/
manifest.json
bin/
MyPlugin.dll
MyPlugin.deps.json
MyPlugin.runtimeconfig.json
Manifest
manifest.json declares the plugin metadata and entry point.
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"authors": ["Squid"],
"description": "Adds custom server behavior.",
"entryAssembly": "bin/MyPlugin.dll",
"entryType": "MyPlugin.MyPlugin",
"dependencies": [
{
"id": "moongate.dialogue",
"versionRange": ">=1.0.0",
"optional": false
}
]
}
Rules:
idmust be unique- required dependencies must exist
- dependency cycles fail startup
- plugins load in dependency order
Lifecycle
Plugins implement IMoongatePlugin from Moongate.Plugin.Abstractions.
using Moongate.Plugin.Abstractions.Interfaces;
namespace MyPlugin;
public sealed class MyPlugin : IMoongatePlugin
{
public string Id => "my-plugin";
public string Name => "My Plugin";
public string Version => "1.0.0";
public IReadOnlyList<string> Authors => ["Squid"];
public string? Description => "Adds custom server behavior.";
public void Configure(IMoongatePluginContext context)
{
}
public Task InitializeAsync(
IMoongatePluginRuntimeContext context,
CancellationToken cancellationToken
)
{
return Task.CompletedTask;
}
}
The manifest and the plugin entry point should agree on Id, Name, and Version. The runtime uses
the manifest for discovery and dependency resolution, then instantiates the configured entry type.
Configure(...)
Configure(...) runs before the final bootstrap wiring.
Use it to register:
- services
- packet handlers
- game event listeners
- console commands
- file loaders
- persistence descriptors
- Lua user data
- Lua script modules
InitializeAsync(...)
InitializeAsync(...) runs after the runtime is ready.
Use it for lightweight startup work such as:
- resolving services
- reading plugin-local data
- initializing plugin caches
Avoid heavy world mutations here unless they are explicitly part of startup behavior.
Service Access During Initialization
InitializeAsync(...) receives IMoongatePluginRuntimeContext, which exposes:
PluginIdPluginDirectoryServices
Services is a small resolver wrapper. Use it to resolve already-bootstrapped runtime services:
public Task InitializeAsync(
IMoongatePluginRuntimeContext context,
CancellationToken cancellationToken
)
{
var commandService = context.Services.Resolve<ICommandSystemService>();
_ = commandService;
return Task.CompletedTask;
}
Minimal Example
This example registers one console command.
using Moongate.Plugin.Abstractions.Interfaces;
using Moongate.Server.Attributes;
using Moongate.Server.Data.Internal.Commands;
using Moongate.Server.Interfaces.Services.Console;
namespace MyPlugin;
public sealed class MyPlugin : IMoongatePlugin
{
public string Id => "my-plugin";
public string Name => "My Plugin";
public string Version => "1.0.0";
public IReadOnlyList<string> Authors => ["Squid"];
public string? Description => "Example plugin.";
public void Configure(IMoongatePluginContext context)
{
context.RegisterConsoleCommand<HelloPluginCommand>();
}
public Task InitializeAsync(IMoongatePluginRuntimeContext context, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
[RegisterConsoleCommand("hello_plugin", "Example plugin command.")]
public sealed class HelloPluginCommand : ICommandExecutor
{
public Task ExecuteCommandAsync(CommandSystemContext context)
{
context.Print("Hello from plugin.");
return Task.CompletedTask;
}
}
What Plugins Can Extend Today
The plugin bootstrap path can contribute to:
- service registration
- command registration
- packet handler wiring
- game event listener wiring
- file loader registration
- persistence descriptor registration
- Lua module and user-data registration
This keeps plugins aligned with the same runtime systems used by the built-in server.
Dependency Resolution
Dependencies are declared in manifest.json and resolved by plugin id.
Startup fails when:
- two plugins declare the same
id - a required dependency is missing
- a dependency cycle exists
- a declared version requirement is not satisfied
Plugins are loaded, configured, and initialized in dependency order.