Table of Contents

Plugin Development

Warning

⚠️ Please do take note that this documentation may not be entirely up to date, as the server undergoes a lot of changes regularly. If you stumble upon any issues, feel free to contact us on Discord!

Setup development environment

You can follow this guide to setup your development environment.

TLDR: setup a .NET project that references Obsidian.API

Writing your first plugin

Here is a sample plugin that you can start with:

using Obsidian.API;
using Obsidian.API.Plugins;
using Obsidian.API.Plugins.Services;

[Plugin(Name = "My First Plugin", Version = "1.0", Authors = "Plugin developer", Description = "My first plugin.")]
public class MyFirstPlugin : PluginBase
{
    [Inject] public ILogger Logger { get; set; }

    public async Task OnLoad(IServer server)
    {
        Logger.Log($"{Info.Name} loaded! Hello {server.DefaultWorld.Name}!");
    }
}

Here is a small breakdown of this sample plugin:

  • using Obsidian.API: These are your references. Usually you'd only reference Obsidian.API and all libraries you want to use.

  • [Plugin]: This is the Plugin attribute. This attribute also defines a bunch of important info about your plugin:

    • Name: Your plugin's name.
    • Version: Your plugin's version. We recommend using Semantic Versioning.
    • Authors: Who made this plugin. You! :D
    • Description: A description for this plugin, to be listed in Obsidian's /plugins command.
  • : PluginBase: Your plugin class should implement the PluginBase abstract class.

  • [Inject] public ILogger Logger { get; set; } This is a service. More about plugin services can be read here.

  • OnLoad(IServer server): This is an event. These are not the same as .NET's events, as they are invoked with Reflection. More about events can be read here.

  • Logger.Log: This is how you can log messages to the console using the Logger service. It is not recommended to use the Console.Write or Console.WriteLine methods, as these may block server threads slowing down Obsidian.

Plugin Structure

PluginBase

All plugins must derive from the PluginBase class. This is important for receiving events and making the plugin work as a dependency for other plugins. It is the only requirement.

public class MyFirstPlugin : PluginBase
{
}

PluginAttribute

You should add PluginAttribute to provide more information about your plugin.

[Plugin(Name = "My First Plugin", Version = "1.0")]
public class MyFirstPlugin : PluginBase
{
}

You can obtain this information at runtime through the Info property.

Events

Plugin classes can contain methods with a specific signature, which will be called when certain events occur on the server.

public async Task OnPlayerJoin(PlayerJoinEventArgs playerJoinEvent)
{
    var player = playerJoinEvent.Player;
    await player.SendMessageAsync(IChatMessage.Simple($"Welcome {player.Username}!"));
}

Currently available events

(more will be added in the future)

Event Arguments Description
OnLoad IServer Gets called when the plugin loads for the first time
OnIncomingChatMessage IncomingChatMessageEventArgs Gets called when client sends a chat message to the server
OnInventoryClick InventoryClickEventArgs Gets called when player clicks on a slot in a window
OnPermissionGranted PermissionGrantedEventArgs Gets called when player is granted a permission
OnPermissionRevoked PermissionRevokedEventArgs Gets called when player has permission revoked
OnPlayerJoin PlayerJoinEventArgs Gets called when player joins the server (before chunks get sent)
OnPlayerLeave PlayerLeaveEventArgs Gets called after player leaves the server
OnPlayerTeleported PlayerTeleportEventArgs Gets called when player gets teleported
OnServerStatusRequest ServerStatusRequestEventArgs Gets called when client asks for server state
OnServerTick Gets called at the beginning of each server tick

At the moment, all events (except for OnLoad) must return a Task. If your method doesn't need to be asynchronous, you can return Task.CompletedTask

Services

Obsidian provides services, each one with specific functionality. If marked with InjectAttribute, they are automatically injected when the plugin is loaded.

public class MyFirstPlugin : PluginBase
{
    [Inject] public ILogger Logger { get; set; }
}

Currently available services

Service Description
IDiagnoser Used for process diagnoses.
IFileReader Used for reading from files.
IFileWriter Used for creating and writing to files.
ILogger Used to perform logging.
INativeLoader Used for loading and using native libraries.
INetworkClient Not implemented

This list is not final, more services will be added in the future.

How to use services

If marked with InjectAttribute, services are automatically injected when the plugin is loaded. Some services have IsUsable property, meaning that they need a certain permission to be used. If you call security-critical method when service isn't usable, a SecurityException will be thrown.

public class MyFirstPlugin : PluginBase
{
    [Inject] public IFileWriter FileWriter { get; set; }

    public async Task OnLoad(IServer server)
    {
        if (FileWriter.IsUsable)
        {
            FileWriter.WriteAllText("file.txt", "content");
        }
    }
}

Plugin permissions

Plugins could potentially contain malicious code. To avoid plugins from accessing local files, sending network requests, running subprocesses, compiling their own code etc., we added a plugin permission system. A server owner can grant and revoke permissions to do security-critical actions for each plugin. If your plugin references libraries like System.IO or System.Reflection, it needs corresponding permissions to be granted in order to run.

Secured Services

If you want your plugin to run, even though it's missing some permissions, you can use secured services. You can read more about services here. Secured services should provide an alternative to security-critical system libraries. If your plugin doesn't have permissions to use such services, calling its methods will throw a SecurityException. You can check whether you have permissions to use the service through the ISecuredService.IsUsable property. A list of ALL services can be found here.

Some methods in secured services are usable even when IsUsable is false, because they are not security-critical themselves. You can tell by the "Exceptions:" in method description:

Example of a secure service call.

Permissions list

Permission Description
CanWrite Allows writing to files.
CanRead Allows reading from files.
NetworkAccess Allows performing actions over network.
Interop Allows using native libraries.
Reflection Allows performing reflection.
RunningSubprocesses Allows using System.Diagnostics and System.Runtime.Loader libraries.
Compilation Allows using Microsoft.CodeAnalysis and related libraries.
ThirdPartyLibraries Allows using 3rd party libraries.

What are plugin dependencies

Plugin dependencies are other loaded plugins, which you can reference and use in your code. In this article you can find out how to reference them through PluginBase. Another way is using dependency wrappers.

How to reference dependencies

You can reference plugins by adding a property/field of type PluginBase and applying DependencyAttribute onto it. When another plugin is loaded and matches the name of your property/field name, it will be injected automatically.


[Dependency]
public PluginBase AnotherPlugin { get; set; }

AnotherPlugin is going to be null, until plugin named "AnotherPlugin" is loaded.

If you want to name your property/field differently than the target dependency, you can use AliasAttribute as in the example:

[Dependency, Alias("AnotherPlugin")]
public PluginBase MyDependency { get; set; }

Aliases can match both class name and PluginAttribute name of the dependency.

All dependencies are "required" by default. Your plugin won't load until all required dependencies are present. You can set dependency to be optional like so:

[Dependency(Optional = true)]
public PluginBase AnotherPlugin { get; set; }

Don't forget to check optional dependencies for null, since their presence is not guaranteed.

Since older versions of plugins may be missing certain functionality that you need, you can set minimal dependency version. All plugins with lower version will be ignored, even if their name matches.


[Dependency(MinVersion = "2.0")]
public PluginBase AnotherPlugin { get; set; }

The version string should contain major, minor, [build], and [revision] numbers, split by a period character ('.')

How to use dependencies

You can obtain dependencies full information with PluginBase.Info

Calling dependencies methods can be done with object Invoke(string, object[]), object InvokeAsync(string, object[]) or generic T Invoke<T>(string, object[]), Task<T> InvokeAsync<T>(string, object[]):

if (dependency != null)
{
    int sum = dependency.Invoke<int>("Add", 1, 2);
}

Since this approach is using reflection, it's rather slow. For frequent calls you should cache delegate obtained with T GetMethod<T>(string, Type[]). There are special methods for obtaining property getters and setters - Func<T> GetPropertyGetter<T>(string) and Action<T> GetPropertySetter<T>(string).

var addMethod = dependency.GetMethod<Func<int, int, int>>("Add", new[] { typeof(int), typeof(int) });
int result = addMethod(1, 2);

var getFoo = dependency.GetPropertyGetter<int>("Foo");
int foo = getFoo();

var setBar = dependency.GetPropertySetter<int>("Bar");
setBar(foo);

For more complex scenarios, it's convenient to use dependency wrappers.

Dependency Wrappers

You should read about dependencies first.

Why wrappers?

Let's say that you want to use this plugin as a dependency:

public class CarPlugin : PluginBase
{
    public string CarName { get; set; }
    public void StartEngine() { /* CODE */ }
}

...and you need to access both CarName and StartEngine.

Normally you might do something like this:

public class MyPlugin : PluginBase
{
    [Dependency]
    public PluginBase CarPlugin { get; set; }

    private Func<string> getCarName;
    private Action<string> setCarName;
    private Action startEngine;

    public void OnLoad(IServer server)
    {
        getCarName = CarPlugin.GetPropertyGetter<string>("CarName");
        setCarName = CarPlugin.GetPropertySetter<string>("CarName");
        startEngine = CarPlugin.GetMethod<Action>("StartEngine");
    }
}

But there is a better way!

All you need to do is make new class that derives from PluginWrapper with name-matching properties and use that as a dependency instead!

public class MyPlugin : PluginBase
{
    [Dependency]
    public CarPluginWrapper CarPlugin { get; set; }

    public class CarPluginWrapper : PluginWrapper
    {
        public Func<string> get_CarName { get; set; }
        public Action<string> set_CarName { get; set; }
        public Action StartEngine { get; set; }
    }
}

Property names don't need to match if you use AliasAttribute:

public class CarPluginWrapper : PluginWrapper
{
    [Alias("get_CarName")]
    public Func<string> GetCarName { get; set; }
    [Alias("set_CarName")]
    public Action<string> SetCarName { get; set; }
    public Action StartEngine { get; set; }
}

Then you can even simulate accessing properties like so:

public class CarPluginWrapper : PluginWrapper
{
    [Alias("get_CarName")]
    private Func<string> GetCarName { get; set; }
    [Alias("set_CarName")]
    private Action<string> SetCarName { get; set; }
    
    public string CarName { get => GetCarName(); set => SetCarName(value); }
}

Chat Messages

Warning!

You shouldn't implement the IChatMessage interface yourself. Use the IChatMessage.CreateNew method to create a new instance instead.

How to create a chat message

There are multiple ways of doing this:

var chatMessage = IChatMessage.Empty;
var chatMessage = IChatMessage.Simple("Hello World!");

chatMessage = IChatMessage.Simple("Hello World!", ChatColor.Gold);
var chatMessage = IChatMessage.CreateNew().AppendText("Hello World!");

chatMessage = IChatMessage.CreateNew().AppendText("Hello World!", ChatColor.Gold);

How to send chat message

The most basic ways of sending chat messages are with an IPlayer:

await player.SendMessageAsync(chatMessage);

or with IServer:

await server.BroadcastAsync(chatMessage);

Adding colors

You can add color codes directly into chat message. For example "§aHello §fWorld!".

Another way is using ChatColor like so: $"{ChatColor.BrightGreen}Hello {ChatColor.White}World!".

This is a list of chat colors and corresponding their color codes:

ChatColor Color code
Black §0
DarkBlue §1
DarkGreen §2
DarkCyan §3
DarkRed §4
Purple §5
Gold §6
Gray §7
DarkGray §8
Blue §9
BrightGreen §a
Cyan §b
Red §c
Pink §d
Yellow §e
White §f

Adding events

Events can be added to text with ITextComponent. Same as with IChatMessage, you shouldn't implement this interface, but use CreateNew method instead.

var textComponent = ITextComponent.CreateNew(ETextAction.ShowText, "☺");

where ETextAction can be one of the following:

  • OpenUrl
  • RunCommand
  • SuggestCommand
  • ChangePage
  • ShowText
  • ShowItem
  • ShowEntity

Then, you can set the component to be activated upon Click/Hover event as in the example:

var chatMessage = IChatMessage.CreateNew();

chatMessage.ClickEvent = textComponent;
// or
chatMessage.HoverEvent = textComponent;

There is a short way of writing it:

var chatMessage = IChatMessage.CreateNew()
    .WithHoverAction(ETextAction.ShowText, "☺")
    // or
    .WithClickAction(ETextAction.ShowText, "☺");

Extras

You may want your event to only affect one part of the chat message. It is achievable by splitting your chat message into extras, then appending those. For this you should use AddExtra(IChatMessage) method. See the example:

var chatMessage = IChatMessage.Simple("If you want to see something, hover over ")
        .AddExtra(IChatMessage.Simple("this", ChatColor.BrightGreen).WithHoverAction(ETextAction.ShowText, "Amazing!"));

Commands

A good plugin of course comes with commands that control it's behavior. Adding commands with Obsidian is quite easy. To add a command to your plugin, all you need to do is add a method with an attribute and a specific signature to your PluginBase class.

[Command("helloworld", "hello")]
[CommandInfo("Sends Hello world! to chat.", "/helloworld")]
public async Task PluginCommandAsync(CommandContext ctx)
{
	Logger.Log($"Executed Hello World!");
	await ctx.Player.SendMessageAsync("Hello world!");
}

In this example, there are a couple of thing that stand out. Namely:

  • [Command("helloworld", "hello")] to define a command's name, and it's aliases.
  • [CommandInfo("Sends Hello world! to chat.", "/helloworld")] To define a command's description and add aliases, and to describe it's default usage.
  • CommandContext ctx is required as a command's first argument.
  • Task is always the return type.

Command Arguments

Command arguments can easily be added by adding parameters to your command method.

[Command("helloworld", "hello")]
[CommandInfo("Sends Hello world! to chat.", "/helloworld <text>")]
public async Task PluginCommandAsync(CommandContext ctx, string text)
{
	Logger.Log($"Executed Hello World! {text}");
	await ctx.Player.SendMessageAsync($"Hello world! {text}");
}

Remaining text argument

By adding the RemainingAttribute to the last string argument, the command handler will read all remaining command text to a string.

public async Task PluginCommandAsync(CommandContext ctx, [Remaining]string text)

Command classes and subcommands

It is possible to define a complex class structure to define commands and subcommands. Do it as following. Take a good look at the comments for more detailed information about what is going on.

[CommandRoot]
public class MyCommandRoot
{
	[Command("helloworld", "hello")]
	[CommandInfo("Sends Hello world! to chat.", "/helloworld")]
	public async Task PluginCommandAsync(CommandContext ctx)
	{
		Logger.Log($"Executed Hello World!");
		await ctx.Player.SendMessageAsync("Hello world!");
	}
	
	/// This is a command group. It can nest multiple commands, and other command groups "infinitely".
	[CommandGroup("whatsup")]
	[CommandInfo("Another command")]
	public class MySubCommandRoot
	{
		[GroupCommand]
		public async Task DefaultCommandAsync(CommandContext ctx)
		{
			// This is the default command for this command group.
			// This will be executed as '/whatsup'
		}
		
		[Command("yo", "hee hee")]
		[CommandInfo("More commands.")]
		public async Task SubCommandAsync(CommandContext ctx)
		{
			// This is a subcommand for this command group.
			// This will be executed as '/whatsup yo'
		}
	}
}

Command classes marked with CommandRoot will automatically have all their subcommands registered.

Pre-execution checks

Commands can have pre-execution checks. These are executed before a command will be executed. These can be enabled as easily as adding an additional attribute to your command.

Currently, only the RequirePermissionAttribute is implemented. Custom execution checks can be implemented by developers as they please.

[RequirePermission(PermissionCheckType.All, true, "MyPlugin.Permission")]

// Arguments in order: Whether All or Any permission should be granted, Whether this command also works with op, Permissions to check.

TODO improve

Command Dependencies

To register dependencies for your command classes, build a new CommandDependencyBundle.

var bundle = new CommandDependencybundle();
await bundle.RegisterDependencyAsync(Logger);

There are two ways to use this dependency in your commands.

Option 1

The first option is to add a Constructor to your CommandRoot and your CommandGroup marked classes.

public Logger MyLogger;

public MyCommandRoot(Logger myLogger)
{
	this.MyLogger = myLogger;
}

NOTE: Command classes do NOT persist across command executions. Beware!

Option 2

Another option is using the CommandContext to obtain dependencies.

// This would be in a command method.
var myLogger = await ctx.Dependencies.GetDependencyAsync<ILogger>();

Creating custom argument handlers

TODO

Creating custom pre-execution checks

TODO