Falcon Plugin API
Plugin API for the Falcon Minecraft: Bedrock Edition server
The API plugins use to extend Falcon. Every language sits on top of the same stable C ABI, so the server exposes each feature once and all plugins get it.
native/- the C ABI (falcon_api.h) and header-only C++ classes built on top of itinternal/- support for internal plugins, which use the server's own C++ classesdotnet/- theFalcon.PluginAPIpackage for C# plugins: interop generated fromfalcon_api.hat build time, C# classes mirroring the C++ ones, and the loader the server runs through the .NET runtimejava/- thefalcon-plugin-apiMaven artifact for Java plugins: bindings generated fromfalcon_api.hat build time with the Foreign Function & Memory API, Java classes mirroring the C++ ones, and the loader the server runs in its embedded Java virtual machineexamples/- complete plugins, built by CI on every supported platform
The C++ headers only talk to the server through the C ABI, so a plugin built with any compiler runs on any Falcon build of the same API major version.
Native ("runtime": "native") |
Internal ("runtime": "internal") |
|
|---|---|---|
| Talks to the server through | the C ABI | the server's own headers and classes |
| Can do | what the API exposes | anything a fork of Falcon can do |
| Runs on | any server with the same API major version | only the exact server build it was compiled against |
| Compiler | any | the one that built the server, same version |
An internal plugin includes headers such as Block/Block.h or Actor/Mob/MobActor.h, subclasses Block,
Item, MobActor, Goal or Command, registers them with FALCON_REGISTER_BLOCK, FALCON_REGISTER_ITEM
and FALCON_REGISTER_ACTOR, and calls any public function of the server. A lower priority number overrides a
vanilla class.
The server refuses an internal plugin built against another commit, build number or compiler, and logs why. Rebuild the plugin for every server update. Keep its classes in a namespace so their names never collide with the server's.
The API is a header-only CMake target named Falcon::PluginAPI. falcon_add_plugin creates the shared
library with the right settings:
include(FetchContent)
FetchContent_Declare(
falcon_plugin_api
GIT_REPOSITORY https://github.com/Falcon-MC/PluginAPI.git
GIT_TAG main
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(falcon_plugin_api)
falcon_add_plugin(MyPlugin MyPlugin.cpp)#include <falcon/Falcon.hpp>
class MyPlugin : public falcon::Plugin {
public:
bool onEnable() override {
events().on<falcon::PlayerJoinEvent>([](falcon::PlayerJoinEvent &event) {
event.player().sendMessage("Welcome!");
});
return true;
}
};
FALCON_PLUGIN(MyPlugin)Put the library next to a plugin.json in plugins/<name>/ on the server:
{
"name": "MyPlugin",
"version": "1.0.0",
"api-version": "1.0",
"main": "MyPlugin"
}Plugins talk to each other through the server, whatever language they are written in. A plugin provides a named service and any other plugin calls it with a text request, usually JSON. Custom events work like the server's events, with priorities and cancellation:
services().provide("economy:balance", [](const std::string &request) {
return std::string("{\"balance\":250}");
});
std::optional<std::string> balance = services().call("economy:balance", "{\"player\":\"Steve\"}");
services().on("quests:completed", [](falcon::CustomEvent &event) {
event.setData(event.data() + " rewarded");
});
falcon::CustomEventResult result = services().fire("quests:completed", "{\"quest\":\"miner\"}", true);A service belongs to the plugin that provided it and disappears when that plugin is disabled. Call services and fire custom events from the main thread.
A C# plugin is a net8.0 class library that references Falcon.PluginAPI and sets EnableDynamicLoading:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Falcon.PluginAPI" Version="1.3.0" />
</ItemGroup>
</Project>using Falcon;
namespace MyPlugin
{
public sealed class Main : Plugin
{
public override bool OnEnable()
{
Events.On<PlayerJoinEvent>(joinEvent =>
{
joinEvent.Player.SendMessage("Welcome!");
});
return true;
}
}
}Copy the output of dotnet publish to plugins/<name>/. The manifest uses the dotnet runtime, main is the
full class name and assembly defaults to <name>.dll:
{
"name": "MyPlugin",
"version": "1.0.0",
"main": "MyPlugin.Main",
"runtime": "dotnet",
"assembly": "MyPlugin.dll"
}The server needs the .NET 8 runtime or newer. It looks for it in DOTNET_ROOT, then in the default install
folders. All C# plugins share one runtime and one Falcon.PluginAPI, which the server loads from
plugins/.dotnet/ (Falcon.PluginAPI.dll and Falcon.PluginAPI.runtimeconfig.json), or from the folder of
the first C# plugin when plugins/.dotnet/ does not exist. Keep that copy at least as new as the one the
plugins were built against. Each plugin gets its own AssemblyLoadContext, so plugins can ship different
versions of the same library.
A plugin can reference another plugin's assembly at compile time and call its classes directly, with the same
types on both sides, as long as it lists that plugin in depend or softdepend: its assemblies and the
libraries they loaded then come from that plugin instead of a second copy.
A Java plugin is a Java 22 jar that depends on falcon-plugin-api with the provided scope:
<dependency>
<groupId>io.github.falcon-mc</groupId>
<artifactId>falcon-plugin-api</artifactId>
<version>1.3.0</version>
<scope>provided</scope>
</dependency>package myplugin;
import falcon.api.Plugin;
import falcon.api.event.PlayerJoinEvent;
public final class MyPlugin extends Plugin {
@Override
public boolean onEnable() {
events().on(PlayerJoinEvent.class, event -> {
event.player().sendMessage("Welcome!");
});
return true;
}
}Put the jar next to a plugin.json in plugins/<name>/. The manifest uses the java runtime, main is the
full class name and jar defaults to <name>.jar:
{
"name": "MyPlugin",
"version": "1.0.0",
"api-version": "1.3",
"main": "myplugin.MyPlugin",
"runtime": "java",
"jar": "MyPlugin.jar"
}The server needs Java 22 or newer. It looks for it in FALCON_JAVA_HOME, then in JAVA_HOME, and passes the
options in FALCON_JAVA_OPTIONS (for example -Xmx1G) to the virtual machine. All Java plugins share one
virtual machine and one falcon-plugin-api-<version>.jar, which the server loads from plugins/.java/: keep it
at least as new as the one the plugins were built against. Each plugin gets its own class loader, and objects
that own native memory, such as a created Item, are AutoCloseable.
A plugin can compile against another plugin's jar (provided scope) and call its classes directly, with the
same classes on both sides, as long as it lists that plugin in depend or softdepend: its class loader then
looks in that plugin's jar before its own, so a class is never loaded twice.
falcon_add_internal_plugin builds the library against one server release. It adds the server's include
directories, links against the server executable and compiles in the server's build identity. Every Falcon
release publishes the matching SDK, FalconSDK-<platform>.tar.gz:
falcon_add_internal_plugin(MyPlugin MyPlugin.cpp)#include "Block/BlockClassRegistry.h"
#include "Block/Blocks/SaplingBlock.h"
#include <falcon/internal/InternalPlugin.hpp>
namespace myplugin {
class CustomSaplingBlock : public SaplingBlock {
public:
using SaplingBlock::SaplingBlock;
static bool matches(const std::string &identifier) {
return SaplingBlock::matches(identifier);
}
};
FALCON_REGISTER_BLOCK(CustomSaplingBlock, 10);
class MyPlugin : public falcon::internal::Plugin {
public:
bool onEnable() override {
subscribe(getServer().getEventBus().after().mPlayerJoin, [](PlayerJoinAfterEvent &event) {
event.mPlayer.sendMessage("Welcome!");
});
return true;
}
};
}
FALCON_INTERNAL_PLUGIN(myplugin::MyPlugin)The manifest uses the internal runtime and needs no api-version:
{
"name": "MyPlugin",
"version": "1.0.0",
"main": "MyPlugin",
"runtime": "internal"
}Register classes with the macros or in onLoad: the server applies them after every plugin has loaded, and
removes them when the plugin is disabled. The library itself stays loaded until the server exits.
On Windows the plugin imports its symbols from FalconServer.exe, so the server executable must keep that
name.
Only CMake 3.16+ and a C++17 compiler are required. The example plugins are built when the project is the
top level project (FALCON_PLUGIN_API_BUILD_EXAMPLES).
cmake -B build -G Ninja
cmake --build build
Internal plugins are built against one server release, with the compiler named in the SDK's README.txt.
Extract the FalconSDK-<platform>.tar.gz of that release and point FALCON_SDK_DIR at it:
cmake -B build -G Ninja -DFALCON_SDK_DIR=/path/to/FalconSDK-linux -DFALCON_PLUGIN_API_BUILD_INTERNAL_EXAMPLES=ON
cmake --build build
For a server built from source with FALCON_EXPORT_SYMBOLS (on by default), point FALCON_BUILD_DIR at its
build directory instead, or build the plugins inside the server's build tree with
-DFALCON_BUILD_INTERNAL_PLUGINS=ON.
The C# package and its example need the .NET 8 SDK. The interop is generated from falcon_api.h on every
build, and the package version follows the API version of that header:
dotnet build examples/dotnet/HelloPlugin/HelloPlugin.csproj -c Release
dotnet pack dotnet/Falcon.PluginAPI/Falcon.PluginAPI.csproj -c Release -o nupkg
The Java artifact and its example need JDK 22 and Maven. The bindings are generated from falcon_api.h on every
build, and the build fails when the artifact version does not match the API version of that header:
mvn -B -f java/pom.xml install
mvn -B -f examples/java/HelloPlugin/pom.xml package
Falcon Plugin API is licensed under the GNU Lesser General Public License v3.0, which supplements the GNU General Public License v3.0. Plugins can use it under any license, as long as changes to the API itself stay under the same license.
Falcon is not affiliated with Mojang. All brands and trademarks belong to their respective owners.