Skip to content

Latest commit

 

History

History
541 lines (422 loc) · 20.6 KB

README.md

File metadata and controls

541 lines (422 loc) · 20.6 KB

Falcon
Falcon Plugin API
Plugin API for the Falcon Minecraft: Bedrock Edition server

CI API Languages Platform

What is this?

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 it
  • internal/ - support for internal plugins, which use the server's own C++ classes
  • dotnet/ - deprecated, kept for existing C# plugins and given no new API: the Falcon.PluginAPI package: interop generated from falcon_api.h at build time, C# classes mirroring the C++ ones, and the loader the server runs through the .NET runtime
  • java/ - the falcon-plugin-api Maven artifact for Java plugins: bindings generated from falcon_api.h at 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 machine
  • wasm/ - the API for WebAssembly plugins: falcon.wit, which mirrors falcon_api.h, and a small C helper (wasm/c/falcon_wasm.h) over the bindings wit-bindgen generates from it
  • examples/ - complete plugins, built by CI on every supported platform except the Wasm one, which builds with its own build.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 and internal plugins

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.

Usage

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"
}

Services and custom events

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.

Effects, attributes, sounds and particles

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.

Scoreboard

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));

Containers and signs

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.

Persistent data

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.

Typed commands, running commands and recipes

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");

World

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);

Entities and players

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);

Administration

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("?"));

More events

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();
});

C# plugins

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.

Java plugins

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.

Internal plugins

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.

Building

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

Related repositories

Licensing information

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.