From a5293f6db8955b535bb7f8e8ad67a187d535514b Mon Sep 17 00:00:00 2001 From: NaySurGithub <96527285+NaySurGithub@users.noreply.github.com> Date: Sat, 14 Nov 2026 21:23:30 +0100 Subject: [PATCH] feat: add typed command overloads, command execution and recipes to the plugin api --- README.md | 25 +++++++ .../hello/commands/GiveDiamondCommand.java | 6 ++ .../src/Commands/GiveDiamondCommand.cpp | 3 + java/src/main/java/falcon/api/Player.java | 9 +++ java/src/main/java/falcon/api/Recipes.java | 69 +++++++++++++++++++ java/src/main/java/falcon/api/Server.java | 4 ++ .../falcon/api/command/CommandParamType.java | 26 +++++++ .../falcon/api/command/CommandParameter.java | 20 ++++++ .../java/falcon/api/command/Commands.java | 35 ++++++++++ native/include/falcon/Commands.hpp | 48 +++++++++++++ native/include/falcon/Falcon.hpp | 1 + native/include/falcon/Player.hpp | 5 ++ native/include/falcon/Recipes.hpp | 46 +++++++++++++ native/include/falcon/falcon_api.h | 40 +++++++++++ wasm/falcon.wit | 32 +++++++++ 15 files changed, 369 insertions(+) create mode 100644 java/src/main/java/falcon/api/Recipes.java create mode 100644 java/src/main/java/falcon/api/command/CommandParamType.java create mode 100644 java/src/main/java/falcon/api/command/CommandParameter.java create mode 100644 native/include/falcon/Recipes.hpp diff --git a/README.md b/README.md index f4543d3..1e5eca3 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,31 @@ Java plugins reach the same store through `plugin.data()`. `Item::nbt` and `Item item's whole tag as little-endian NBT, the game's own format, and `Entity::nbt` returns what the server would save for an entity. +### 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: + +```cpp +commands().add("warp", "Teleports to a warp", "/warp ", 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: + +```cpp +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"); +``` + ### C# plugins The `dotnet` runtime is deprecated. Write new plugins for Wasm, native C++ or Java. diff --git a/examples/java/HelloPlugin/src/main/java/falcon/examples/hello/commands/GiveDiamondCommand.java b/examples/java/HelloPlugin/src/main/java/falcon/examples/hello/commands/GiveDiamondCommand.java index 18dea7e..b021c6e 100644 --- a/examples/java/HelloPlugin/src/main/java/falcon/examples/hello/commands/GiveDiamondCommand.java +++ b/examples/java/HelloPlugin/src/main/java/falcon/examples/hello/commands/GiveDiamondCommand.java @@ -6,6 +6,8 @@ import falcon.api.Player; import falcon.api.Plugin; import falcon.api.command.CommandContext; +import falcon.api.command.CommandParamType; +import falcon.api.command.CommandParameter; import falcon.api.command.CommandPermission; import java.util.List; @@ -22,6 +24,10 @@ public static void registerTo(Plugin plugin) { plugin.permissions().add(PERMISSION, PermissionDefault.OPERATOR); plugin.commands().add("give-diamond", "Gives you a shiny diamond", "/give-diamond [count]", GiveDiamondCommand::execute, CommandPermission.ANY); + plugin.commands().addOverload("give-diamond", + List.of(CommandParameter.optional("count", CommandParamType.INT))); + plugin.server().recipes().addShapeless("hello:diamond_from_coal", List.of("minecraft:coal", "minecraft:coal", + "minecraft:coal", "minecraft:coal"), DIAMOND, 1); } private static boolean execute(CommandContext context) { diff --git a/examples/native/HelloPlugin/src/Commands/GiveDiamondCommand.cpp b/examples/native/HelloPlugin/src/Commands/GiveDiamondCommand.cpp index c4eba54..e707245 100644 --- a/examples/native/HelloPlugin/src/Commands/GiveDiamondCommand.cpp +++ b/examples/native/HelloPlugin/src/Commands/GiveDiamondCommand.cpp @@ -44,4 +44,7 @@ void GiveDiamondCommand::registerTo(const falcon::Plugin &plugin) { context.reply("Here you go!"); return true; }, falcon::CommandPermission::Any); + plugin.commands().addOverload("give-diamond", {{"count", falcon::CommandParamType::Int, true}}); + falcon::Recipes::addShapeless("hello:diamond_from_coal", + {"minecraft:coal", "minecraft:coal", "minecraft:coal", "minecraft:coal"}, DIAMOND); } diff --git a/java/src/main/java/falcon/api/Player.java b/java/src/main/java/falcon/api/Player.java index c3318c4..c189a4c 100644 --- a/java/src/main/java/falcon/api/Player.java +++ b/java/src/main/java/falcon/api/Player.java @@ -142,6 +142,15 @@ public void sendActionBar(String message) { } } + /** + * Runs a command line, without the slash, as this player, with the player's permissions. + */ + public boolean executeCommand(String commandLine) { + try (Arena arena = Arena.ofConfined()) { + return api().playerExecuteCommand(mHandle, Interop.text(arena, commandLine)) != 0; + } + } + public void playSound(String sound, Vec3 position) { playSound(sound, position, 1.0f, 1.0f); } diff --git a/java/src/main/java/falcon/api/Recipes.java b/java/src/main/java/falcon/api/Recipes.java new file mode 100644 index 0000000..4f29cd1 --- /dev/null +++ b/java/src/main/java/falcon/api/Recipes.java @@ -0,0 +1,69 @@ +package falcon.api; + +import falcon.api.internal.FalconAbi; +import falcon.api.internal.Interop; +import falcon.api.internal.NativeApi; +import falcon.api.internal.PluginContext; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.util.List; + +/** + * Crafting table recipes. Players online get the new recipe book at once. A removed vanilla recipe is gone until the + * server restarts. + */ +public final class Recipes { + private final PluginContext mContext; + + Recipes(PluginContext context) { + mContext = context; + } + + /** + * Ingredients row by row, width x height of them; an empty string leaves the cell empty. + */ + public boolean addShaped(String identifier, int width, int height, List ingredients, String result, + int count) { + return add(identifier, true, width, height, ingredients, result, count); + } + + public boolean addShapeless(String identifier, List ingredients, String result, int count) { + return add(identifier, false, 0, 0, ingredients, result, count); + } + + /** + * Removes a recipe added by a plugin or a pack, or a vanilla crafting recipe by its identifier. + */ + public boolean remove(String identifier) { + try (Arena arena = Arena.ofConfined()) { + return api().removeRecipe(Interop.text(arena, identifier)) != 0; + } + } + + private boolean add(String identifier, boolean shaped, int width, int height, List ingredients, + String result, int count) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment items = arena.allocate(ValueLayout.ADDRESS, Math.max(ingredients.size(), 1)); + for (int index = 0; index < ingredients.size(); index++) { + items.setAtIndex(ValueLayout.ADDRESS, index, Interop.text(arena, ingredients.get(index))); + } + + MemorySegment descriptor = arena.allocate(FalconAbi.RecipeDescriptor.LAYOUT); + descriptor.set(ValueLayout.ADDRESS, FalconAbi.RecipeDescriptor.IDENTIFIER, Interop.text(arena, identifier)); + descriptor.set(ValueLayout.JAVA_INT, FalconAbi.RecipeDescriptor.SHAPED, shaped ? 1 : 0); + descriptor.set(ValueLayout.JAVA_INT, FalconAbi.RecipeDescriptor.WIDTH, width); + descriptor.set(ValueLayout.JAVA_INT, FalconAbi.RecipeDescriptor.HEIGHT, height); + descriptor.set(ValueLayout.ADDRESS, FalconAbi.RecipeDescriptor.INGREDIENTS, items); + descriptor.set(ValueLayout.JAVA_INT, FalconAbi.RecipeDescriptor.INGREDIENT_COUNT, ingredients.size()); + descriptor.set(ValueLayout.ADDRESS, FalconAbi.RecipeDescriptor.RESULT, Interop.text(arena, result)); + descriptor.set(ValueLayout.JAVA_INT, FalconAbi.RecipeDescriptor.RESULT_COUNT, count); + return api().registerRecipe(mContext.handle(), descriptor) != 0; + } + } + + private static NativeApi api() { + return Interop.api(); + } +} diff --git a/java/src/main/java/falcon/api/Server.java b/java/src/main/java/falcon/api/Server.java index 689d30c..4766c84 100644 --- a/java/src/main/java/falcon/api/Server.java +++ b/java/src/main/java/falcon/api/Server.java @@ -101,6 +101,10 @@ public Scoreboard scoreboard() { return new Scoreboard(mContext); } + public Recipes recipes() { + return new Recipes(mContext); + } + private static NativeApi api() { return Interop.api(); } diff --git a/java/src/main/java/falcon/api/command/CommandParamType.java b/java/src/main/java/falcon/api/command/CommandParamType.java new file mode 100644 index 0000000..192e4f9 --- /dev/null +++ b/java/src/main/java/falcon/api/command/CommandParamType.java @@ -0,0 +1,26 @@ +package falcon.api.command; + +import falcon.api.internal.FalconAbi; + +public enum CommandParamType { + INT(FalconAbi.COMMAND_PARAM_INT), + FLOAT(FalconAbi.COMMAND_PARAM_FLOAT), + STRING(FalconAbi.COMMAND_PARAM_STRING), + BOOL(FalconAbi.COMMAND_PARAM_BOOL), + TARGET(FalconAbi.COMMAND_PARAM_TARGET), + POSITION(FalconAbi.COMMAND_PARAM_POSITION), + BLOCK_POSITION(FalconAbi.COMMAND_PARAM_BLOCK_POSITION), + MESSAGE(FalconAbi.COMMAND_PARAM_MESSAGE), + JSON(FalconAbi.COMMAND_PARAM_JSON), + ENUM(FalconAbi.COMMAND_PARAM_ENUM); + + private final int mValue; + + CommandParamType(int value) { + mValue = value; + } + + public int value() { + return mValue; + } +} diff --git a/java/src/main/java/falcon/api/command/CommandParameter.java b/java/src/main/java/falcon/api/command/CommandParameter.java new file mode 100644 index 0000000..db87cb6 --- /dev/null +++ b/java/src/main/java/falcon/api/command/CommandParameter.java @@ -0,0 +1,20 @@ +package falcon.api.command; + +import java.util.List; + +/** + * One parameter of a command overload. The values of an enum parameter are the choices the client completes. + */ +public record CommandParameter(String name, CommandParamType type, boolean optional, List values) { + public static CommandParameter of(String name, CommandParamType type) { + return new CommandParameter(name, type, false, List.of()); + } + + public static CommandParameter optional(String name, CommandParamType type) { + return new CommandParameter(name, type, true, List.of()); + } + + public static CommandParameter choice(String name, List values) { + return new CommandParameter(name, CommandParamType.ENUM, false, List.copyOf(values)); + } +} diff --git a/java/src/main/java/falcon/api/command/Commands.java b/java/src/main/java/falcon/api/command/Commands.java index d492838..59cd8c5 100644 --- a/java/src/main/java/falcon/api/command/Commands.java +++ b/java/src/main/java/falcon/api/command/Commands.java @@ -49,6 +49,41 @@ public boolean add(String name, String description, String usage, CommandHandler } } + /** + * Adds a typed signature to a command of this plugin; the client shows it and completes its arguments. The handler + * still receives the arguments as text, one per word. The first overload replaces the default one. + */ + public boolean addOverload(String command, List parameters) { + try (Arena arena = Arena.ofConfined()) { + long size = FalconAbi.CommandParameter.LAYOUT.byteSize(); + MemorySegment array = arena.allocate(FalconAbi.CommandParameter.LAYOUT, Math.max(parameters.size(), 1)); + for (int index = 0; index < parameters.size(); index++) { + CommandParameter parameter = parameters.get(index); + MemorySegment entry = array.asSlice(index * size, size); + MemorySegment values = arena.allocate(ValueLayout.ADDRESS, Math.max(parameter.values().size(), 1)); + for (int value = 0; value < parameter.values().size(); value++) { + values.setAtIndex(ValueLayout.ADDRESS, value, Interop.text(arena, parameter.values().get(value))); + } + entry.set(ValueLayout.ADDRESS, FalconAbi.CommandParameter.NAME, Interop.text(arena, parameter.name())); + entry.set(ValueLayout.JAVA_INT, FalconAbi.CommandParameter.TYPE, parameter.type().value()); + entry.set(ValueLayout.JAVA_INT, FalconAbi.CommandParameter.OPTIONAL, parameter.optional() ? 1 : 0); + entry.set(ValueLayout.ADDRESS, FalconAbi.CommandParameter.VALUES, values); + entry.set(ValueLayout.JAVA_INT, FalconAbi.CommandParameter.VALUE_COUNT, parameter.values().size()); + } + return Interop.api().commandAddOverload(mContext.handle(), Interop.text(arena, command), array, + parameters.size()) != 0; + } + } + + /** + * Runs a command line, without the slash, as the server console. + */ + public boolean execute(String commandLine) { + try (Arena arena = Arena.ofConfined()) { + return Interop.api().serverExecuteCommand(Interop.text(arena, commandLine)) != 0; + } + } + private static int dispatch(MemorySegment sender, MemorySegment arguments, int argumentCount, MemorySegment userData) { try { diff --git a/native/include/falcon/Commands.hpp b/native/include/falcon/Commands.hpp index d30c5a0..6e959f2 100644 --- a/native/include/falcon/Commands.hpp +++ b/native/include/falcon/Commands.hpp @@ -50,6 +50,27 @@ namespace falcon { using CommandHandler = std::function; + enum class CommandParamType : uint32_t { + Int = FALCON_COMMAND_PARAM_INT, + Float = FALCON_COMMAND_PARAM_FLOAT, + String = FALCON_COMMAND_PARAM_STRING, + Bool = FALCON_COMMAND_PARAM_BOOL, + Target = FALCON_COMMAND_PARAM_TARGET, + Position = FALCON_COMMAND_PARAM_POSITION, + BlockPosition = FALCON_COMMAND_PARAM_BLOCK_POSITION, + Message = FALCON_COMMAND_PARAM_MESSAGE, + Json = FALCON_COMMAND_PARAM_JSON, + Enum = FALCON_COMMAND_PARAM_ENUM + }; + + struct CommandParameter { + std::string name; + CommandParamType type = CommandParamType::String; + bool optional = false; + /** The choices of an enum parameter, offered by the client's completion. */ + std::vector values; + }; + class Commands { public: bool add(const std::string &name, const std::string &description, const std::string &usage, @@ -66,6 +87,33 @@ namespace falcon { return detail::api().registerCommand(detail::plugin(), &descriptor) != 0; } + /** + * Adds a typed signature to a command of this plugin; the client shows it and completes its arguments. The + * handler still receives the arguments as text, one per word. The first overload replaces the default one. + */ + bool addOverload(const std::string &command, const std::vector ¶meters) const { + std::vector> values; + std::vector raw; + values.reserve(parameters.size()); + raw.reserve(parameters.size()); + for (const CommandParameter ¶meter: parameters) { + values.emplace_back(); + for (const std::string &value: parameter.values) + values.back().push_back(value.c_str()); + raw.push_back(FalconCommandParameter{parameter.name.c_str(), + static_cast(parameter.type), + parameter.optional ? 1 : 0, values.back().data(), + static_cast(values.back().size())}); + } + return detail::api().commandAddOverload(detail::plugin(), command.c_str(), raw.data(), + static_cast(raw.size())) != 0; + } + + /** Runs a command line, without the slash, as the server console. */ + bool execute(const std::string &commandLine) const { + return detail::api().serverExecuteCommand(commandLine.c_str()) != 0; + } + private: static int dispatch(FalconCommandSender *sender, const char *const *arguments, uint32_t argumentCount, void *userData) { diff --git a/native/include/falcon/Falcon.hpp b/native/include/falcon/Falcon.hpp index bcd9d58..2f1f82c 100644 --- a/native/include/falcon/Falcon.hpp +++ b/native/include/falcon/Falcon.hpp @@ -24,6 +24,7 @@ #include "falcon/PlayerEvents.hpp" #include "falcon/PlayerInventory.hpp" #include "falcon/Plugin.hpp" +#include "falcon/Recipes.hpp" #include "falcon/Scheduler.hpp" #include "falcon/Scoreboard.hpp" #include "falcon/Server.hpp" diff --git a/native/include/falcon/Player.hpp b/native/include/falcon/Player.hpp index 8af9cf6..b1548cd 100644 --- a/native/include/falcon/Player.hpp +++ b/native/include/falcon/Player.hpp @@ -124,6 +124,11 @@ namespace falcon { detail::api().playerSendActionBar(mHandle, message.c_str()); } + /** Runs a command line, without the slash, as this player, with the player's permissions. */ + bool executeCommand(const std::string &commandLine) const { + return detail::api().playerExecuteCommand(mHandle, commandLine.c_str()) != 0; + } + /** Plays a sound only this player hears, from a position in its level. */ void playSound(const std::string &sound, const Vec3 &position, float volume = 1.0f, float pitch = 1.0f) const { detail::api().playerPlaySound(mHandle, sound.c_str(), position.raw(), volume, pitch); diff --git a/native/include/falcon/Recipes.hpp b/native/include/falcon/Recipes.hpp new file mode 100644 index 0000000..cd62907 --- /dev/null +++ b/native/include/falcon/Recipes.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "falcon/Api.hpp" + +#include +#include +#include + +namespace falcon { + /** + * Crafting table recipes. Players online get the new recipe book at once. A removed vanilla recipe is gone until + * the server restarts. + */ + class Recipes { + public: + /** Ingredients row by row, width x height of them; an empty string leaves the cell empty. */ + static bool addShaped(const std::string &identifier, uint32_t width, uint32_t height, + const std::vector &ingredients, const std::string &result, + uint32_t count = 1) { + return add(identifier, true, width, height, ingredients, result, count); + } + + static bool addShapeless(const std::string &identifier, const std::vector &ingredients, + const std::string &result, uint32_t count = 1) { + return add(identifier, false, 0, 0, ingredients, result, count); + } + + /** Removes a recipe added by a plugin or a pack, or a vanilla crafting recipe by its identifier. */ + static bool remove(const std::string &identifier) { + return detail::api().removeRecipe(identifier.c_str()) != 0; + } + + private: + static bool add(const std::string &identifier, bool shaped, uint32_t width, uint32_t height, + const std::vector &ingredients, const std::string &result, uint32_t count) { + std::vector items; + items.reserve(ingredients.size()); + for (const std::string &ingredient: ingredients) + items.push_back(ingredient.c_str()); + + const FalconRecipeDescriptor descriptor{identifier.c_str(), shaped ? 1 : 0, width, height, items.data(), + static_cast(items.size()), result.c_str(), count}; + return detail::api().registerRecipe(detail::plugin(), &descriptor) != 0; + } + }; +} diff --git a/native/include/falcon/falcon_api.h b/native/include/falcon/falcon_api.h index 612611b..04fd196 100644 --- a/native/include/falcon/falcon_api.h +++ b/native/include/falcon/falcon_api.h @@ -201,6 +201,18 @@ typedef uint32_t FalconCommandPermission; #define FALCON_PERMISSION_ANY 0u #define FALCON_PERMISSION_OPERATOR 1u +typedef uint32_t FalconCommandParamType; +#define FALCON_COMMAND_PARAM_INT 0u +#define FALCON_COMMAND_PARAM_FLOAT 1u +#define FALCON_COMMAND_PARAM_STRING 2u +#define FALCON_COMMAND_PARAM_BOOL 3u +#define FALCON_COMMAND_PARAM_TARGET 4u +#define FALCON_COMMAND_PARAM_POSITION 5u +#define FALCON_COMMAND_PARAM_BLOCK_POSITION 6u +#define FALCON_COMMAND_PARAM_MESSAGE 7u +#define FALCON_COMMAND_PARAM_JSON 8u +#define FALCON_COMMAND_PARAM_ENUM 9u + typedef void (*FalconEventHandler)(FalconEvent *event, void *userData); typedef const char *(*FalconServiceHandler)(const char *request, void *userData); typedef void (*FalconTask)(void *userData); @@ -216,6 +228,27 @@ typedef struct FalconCommandDescriptor { void *userData; } FalconCommandDescriptor; +/* one parameter of a command overload; an enum parameter lists its values, which the client completes */ +typedef struct FalconCommandParameter { + const char *name; + FalconCommandParamType type; + int32_t optional; + const char *const *values; + uint32_t valueCount; +} FalconCommandParameter; + +/* a crafting table recipe; shaped: width x height ingredients row by row, NULL or "" for an empty cell */ +typedef struct FalconRecipeDescriptor { + const char *identifier; + int32_t shaped; + uint32_t width; + uint32_t height; + const char *const *ingredients; + uint32_t ingredientCount; + const char *result; + uint32_t resultCount; +} FalconRecipeDescriptor; + typedef int (*FalconItemUseHandler)(FalconPlayer *player, FalconItem *item, void *userData); typedef int (*FalconItemUseOnBlockHandler)(FalconPlayer *player, FalconItem *item, FalconBlockPos position, uint32_t face, void *userData); @@ -577,6 +610,13 @@ typedef struct FalconServerApi { const uint8_t *(*itemNbt)(FalconItem *item, uint32_t *length); int (*itemSetNbt)(FalconItem *item, const uint8_t *data, uint32_t length); const uint8_t *(*entityNbt)(FalconEntity *entity, uint32_t *length); + + int (*commandAddOverload)(FalconPlugin *plugin, const char *command, const FalconCommandParameter *parameters, + uint32_t count); + int (*serverExecuteCommand)(const char *commandLine); + int (*playerExecuteCommand)(FalconPlayer *player, const char *commandLine); + int (*registerRecipe)(FalconPlugin *plugin, const FalconRecipeDescriptor *descriptor); + int (*removeRecipe)(const char *identifier); } FalconServerApi; typedef int (*FalconPluginEntry)(const FalconServerApi *api, FalconPlugin *plugin, FalconPluginCallbacks *callbacks); diff --git a/wasm/falcon.wit b/wasm/falcon.wit index f4b9ee0..e970aa4 100644 --- a/wasm/falcon.wit +++ b/wasm/falcon.wit @@ -149,6 +149,7 @@ interface players { /// `none` stops every sound. player-stop-sound: func(player: player, sound: option); player-spawn-particle: func(player: player, particle: string, position: vec3); + player-execute-command: func(player: player, command-line: string) -> bool; } interface entities { @@ -407,7 +408,19 @@ interface commands { handler: u32, } + /// `kind` is one of the `FALCON_COMMAND_PARAM_*` values; `values` are the choices of an enum parameter. + record command-parameter { + name: string, + kind: u32, + optional: bool, + values: list, + } + register-command: func(descriptor: command-descriptor) -> bool; + /// For a command of this plugin; the first overload replaces the default one. + command-add-overload: func(command: string, parameters: list) -> bool; + /// Runs a command line, without the slash, as the server console. + execute-command: func(command-line: string) -> bool; sender-name: func() -> string; sender-player: func() -> option; sender-send-message: func(message: string); @@ -439,6 +452,24 @@ interface services { call-service: func(name: string, request: string) -> option; } +/// Crafting table recipes. Shaped ingredients go row by row, width x height of them, an empty string for an empty +/// cell. +interface recipes { + record recipe { + identifier: string, + shaped: bool, + width: u32, + height: u32, + ingredients: list, + output: string, + output-count: u32, + } + + register-recipe: func(recipe: recipe) -> bool; + /// A recipe of a plugin or a pack, or a vanilla crafting recipe by its identifier. + remove-recipe: func(identifier: string) -> bool; +} + /// Custom content. A callback left as `none` is not registered, like a null handler in the C API; the value of /// a callback that is set is passed back to the matching `on-*` function. interface content { @@ -517,6 +548,7 @@ world falcon-plugin { import permissions; import services; import content; + import recipes; export plugin; }