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/- deprecated, kept for existing C# plugins and given no new API: theFalcon.PluginAPIpackage: 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 machinewasm/- the API for WebAssembly plugins:falcon.wit, which mirrorsfalcon_api.h, and a small C helper (wasm/c/falcon_wasm.h) over the bindingswit-bindgengenerates from itexamples/- complete plugins, built by CI on every supported platform except the Wasm one, which builds with its ownbuild.sh
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.
Potion effects and attributes work on players and entities alike, through Entity. A duration is in ticks,
falcon::Effect::INFINITE for an endless effect, and an effect weaker or shorter than the running one is refused
as in the game. Attributes are the ones the entity has, by identifier, such as minecraft:movement; a value
outside the attribute's range is refused.
const falcon::Entity body = player.entity();
body.addEffect(falcon::EffectType::Speed, 200, 1);
std::optional<falcon::Attribute> speed = body.attribute("minecraft:movement");
body.setAttribute("minecraft:movement", 0.15f);
player.playSound("random.levelup", body.position());
body.level().playSound("mob.wolf.bark", body.position(), 1.0f, 1.2f);
body.level().spawnParticle("minecraft:heart_particle", body.position());A sound or particle sent through a Player reaches only that player; through a Level, every player in it.
falcon::Scoreboard is the server's own scoreboard, the one /scoreboard and scripts use, saved with the default
world. A participant is an entity, a player through player.entity(), or a fake player named by text:
falcon::Scoreboard::addObjective("kills", "Kills");
falcon::Scoreboard::setDisplay(falcon::DisplaySlot::Sidebar, "kills");
falcon::Scoreboard::setScore("kills", player.entity(), 3);
falcon::Scoreboard::setScore("kills", "Herobrine", 99);
for (const falcon::Score &score: falcon::Scoreboard::scores("kills"))
logger().info(score.name + ": " + std::to_string(score.value));Any container block of a level, chests, barrels, furnaces, hoppers and the rest, is read and written by slot. A player's ender chest is its own inventory, the same in every ender chest block. Players looking into a container see the change at once:
const falcon::BlockPos chest{10, 64, 10};
for (uint32_t slot = 0; slot < level.containerSize(chest); ++slot) {
if (std::optional<falcon::Item> item = level.containerItem(chest, slot))
logger().info(item->identifier());
}
level.setContainerItem(chest, 0, *falcon::Item::create("minecraft:diamond", 3));
player.inventory().setEnderChestItem(0, *falcon::Item::create("minecraft:elytra"));
level.setSignText({12, 64, 10}, "Welcome!");Opening a container window that belongs to no block, such as a chest menu, is not part of the API yet: the server has no block-less container window.
A plugin keeps text on entities, players and items, saved with them. The keys belong to the plugin: two plugins using the same key never see each other's values. A copy of an inventory item has to be put back for its change to stay:
player.entity().setData("rank", "builder");
std::optional<std::string> rank = player.entity().data("rank");
falcon::PlayerInventory inventory = player.inventory();
if (std::optional<falcon::Item> held = inventory.item(inventory.selectedSlot())) {
held->setData("owner", player.uuid());
inventory.setItem(inventory.selectedSlot(), *held);
}Java plugins reach the same store through plugin.data(). Item::nbt and Item::setNbt read and replace an
item's whole tag as little-endian NBT, the game's own format. Entity::nbt returns what the server would save
for an entity, and Entity::setNbt loads a mob or any other non-player entity back from such data.
An overload gives a command typed parameters that the client shows and completes; enum parameters list their choices. The handler still gets its arguments as text. Commands run as the console or as a player:
commands().add("warp", "Teleports to a warp", "/warp <name>", handler, falcon::CommandPermission::Any);
commands().addOverload("warp", {{"name", falcon::CommandParamType::Enum, false, {"spawn", "shop", "arena"}},
{"target", falcon::CommandParamType::Target, true}});
commands().execute("time set day");
player.executeCommand("me waves");Recipes are crafting table recipes, shaped or shapeless; players online get the new recipe book at once. A vanilla crafting recipe is removed by its identifier until the server restarts:
falcon::Recipes::addShaped("myplugin:emerald_block", 2, 2,
{"minecraft:emerald", "minecraft:emerald", "minecraft:emerald", "minecraft:emerald"},
"minecraft:emerald_block");
falcon::Recipes::addShapeless("myplugin:dye", {"minecraft:dandelion"}, "minecraft:yellow_dye", 2);
falcon::Recipes::remove("myplugin:dye");Levels expose the rules and state of their world: game rules, the biome at a position, rays, entities in a box, structures and ticking areas. The difficulty belongs to the server:
level.setGameRule("keepinventory", "true");
std::string biome = level.biome({0, 64, 0});
const falcon::Entity eyes = player.entity();
if (std::optional<falcon::BlockHit> hit = level.raycastBlock(eyes.position(), {0, -1, 0}, 16.0))
player.sendMessage("standing over " + level.blockName(hit->block));
for (const falcon::Entity &nearby: level.entitiesInBox({-8, 60, -8}, {8, 70, 8}))
nearby.setOnFire(60);
level.placeStructure("mystructure:house", {100, 64, 100}, 1);
level.addTickingArea("spawn", {-16, 0, -16}, {16, 0, 16});
falcon::Server::setDifficulty(falcon::Difficulty::Hard);Mobs expose their equipment, riding, tags and attack target; players their spawn point, language, platform, skin,
camera and item cooldowns. Player::setSkin gives a player a classic skin of at most 128 by 128 pixels:
zombie.setEquipment(falcon::EquipmentSlot::Head, *falcon::Item::create("minecraft:diamond_helmet"));
player.entity().mount(horse);
zombie.addTag("boss");
zombie.setAttackTarget(player.entity());
player.setSpawnPoint(level, {0, 80, 0});
player.sendMessage("Your language: " + player.language());
player.setCamera("minecraft:free", {0, 100, 0}, {30, 90, 0}, 2.0f, falcon::CameraEase::InOutSine);
player.fadeCamera(0.5f, 1.0f, 0.5f, 0x000000);falcon::Server reaches the ban lists, the allow list, the server list entry, the tick rate and the other plugins:
falcon::Server::ban("Griefer", "Breaking the spawn");
falcon::Server::allowListAdd("Friend");
falcon::Server::setMotd("Falcon - event tonight");
if (falcon::Server::tps() < 15.0)
falcon::Server::broadcast("The server is under load");
for (const std::string &name: falcon::Server::plugins())
logger().info(name + " " + falcon::Server::pluginVersion(name).value_or("?"));| Event | Cancellable | Carries |
|---|---|---|
PlayerTeleportEvent |
yes | from, to (can change), cause: command, ender_pearl, chorus_fruit, end_gateway, plugin |
PlayerItemUseEvent |
yes | a copy of the held item, for any item used in the air |
PlayerBucketFillEvent, PlayerBucketEmptyEvent |
yes | the bucket and the block it takes from or fills |
PlayerFishEvent |
yes | the hook and the entity it holds, or a caught fish |
PlayerArmorChangeEvent |
no | the slot, the new and the previous item |
PlayerEmoteEvent |
yes | the emote id |
EntityEffectAddEvent |
yes | the effect |
EntityHealEvent |
yes | the amount (can change), cause: heal, regeneration, self_heal, totem |
EntityBreedEvent |
yes | both parents and the baby's type |
EntityTameEvent |
yes | the mob and its tamer |
EntityMountEvent, EntityDismountEvent |
mount only | the rider and the vehicle |
EntityTeleportEvent |
yes | from, to (can change), cause: end_gateway, follow_owner, plugin |
World events:
| Event | Cancellable | Carries |
|---|---|---|
HopperTransferEvent |
yes | the hopper, the item it moves and whether it pushes or pulls |
BlockDispenseEvent |
yes | the dispenser or dropper and the item |
BrewEvent |
yes | the brewing stand and its ingredient |
AnvilUseEvent |
no | the player, the anvil and the levels it cost |
ItemSpawnEvent |
yes | the item and where it appears |
PortalCreateEvent |
yes | the nether portal blocks |
WorldSaveEvent |
no | the world, before it is written |
events().on<falcon::PlayerTeleportEvent>([](falcon::PlayerTeleportEvent &event) {
if (event.cause() == "ender_pearl" && event.to().y > 200)
event.cancel();
});The dotnet runtime is deprecated. Write new plugins for Wasm, native C++ or Java.
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.0.6" />
</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.0.6</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.0.6",
"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.