diff --git a/README.md b/README.md index f167e92..458622b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@

CI API - Languages + Languages Platform

@@ -28,7 +28,10 @@ the same stable C ABI, so the server exposes each feature once and all plugins g - **`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 -- **`examples/`** - complete plugins, built by CI on every supported platform +- **`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. diff --git a/examples/wasm/HelloWasm/.gitignore b/examples/wasm/HelloWasm/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/examples/wasm/HelloWasm/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/examples/wasm/HelloWasm/HelloWasm.c b/examples/wasm/HelloWasm/HelloWasm.c new file mode 100644 index 0000000..50bfd58 --- /dev/null +++ b/examples/wasm/HelloWasm/HelloWasm.c @@ -0,0 +1,19 @@ +#include "falcon_wasm.h" + +static void welcome(void) { + falcon_player player; + if (falcon_event_player(&player)) + falcon_send(player, "Welcome! This message comes from WebAssembly."); +} + +static bool hello(int argc, const char *const *argv) { + falcon_reply("Hello from WebAssembly!"); + return true; +} + +bool falcon_enable(void) { + falcon_on(FALCON_EVENT_PLAYER_JOIN, welcome); + falcon_command("hellowasm", "Answers from a WebAssembly plugin", hello); + falcon_log("HelloWasm is enabled"); + return true; +} diff --git a/examples/wasm/HelloWasm/build.sh b/examples/wasm/HelloWasm/build.sh new file mode 100755 index 0000000..dce0715 --- /dev/null +++ b/examples/wasm/HelloWasm/build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Builds HelloWasm.wasm. Needs wit-bindgen, a clang with the wasm32 target and a WASI sysroot (wasi-sdk has +# both): CLANG and WASI_SYSROOT point at them. +set -euo pipefail + +cd "$(dirname "$0")" +: "${CLANG:=clang}" +: "${WASI_SYSROOT:?set WASI_SYSROOT to a WASI sysroot, such as wasi-sdk/share/wasi-sysroot}" +WIT=../../../wasm + +mkdir -p build +wit-bindgen c "$WIT" --out-dir build + +# The server instantiates the core module wit-bindgen targets, not a component: no `wasm-tools component new`. +"$CLANG" --target=wasm32-wasip1 --sysroot="$WASI_SYSROOT" -mexec-model=reactor -O2 -Ibuild -I"$WIT/c" \ + -o build/HelloWasm.wasm HelloWasm.c "$WIT/c/falcon_wasm.c" build/falcon_plugin.c \ + build/falcon_plugin_component_type.o + +echo "Built build/HelloWasm.wasm" diff --git a/examples/wasm/HelloWasm/plugin.json b/examples/wasm/HelloWasm/plugin.json new file mode 100644 index 0000000..4eb2dd7 --- /dev/null +++ b/examples/wasm/HelloWasm/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "HelloWasm", + "version": "1.0.0", + "runtime": "wasm", + "main": "HelloWasm.wasm", + "description": "Greets players and answers /hellowasm from WebAssembly", + "authors": ["Falcon"] +} diff --git a/wasm/c/falcon_wasm.c b/wasm/c/falcon_wasm.c new file mode 100644 index 0000000..dd964ff --- /dev/null +++ b/wasm/c/falcon_wasm.c @@ -0,0 +1,232 @@ +#include "falcon_wasm.h" + +#include +#include + +struct subscription { + uint64_t id; + falcon_event_handler handler; +}; + +static struct subscription *subscriptions; +static uint32_t subscriptionCount; +static falcon_command_handler *commands; +static uint32_t commandCount; +static falcon_task_handler *tasks; +static uint32_t taskCount; + +static bool grow(void **array, uint32_t count, size_t size) { + void *larger = realloc(*array, (count + 1) * size); + if (larger == NULL) + return false; + *array = larger; + return true; +} + +static void logAt(falcon_plugin_types_log_level_t level, const char *message) { + falcon_plugin_string_t text; + falcon_plugin_string_set(&text, message); + falcon_plugin_server_log(level, &text); +} + +void falcon_log(const char *message) { + logAt(FALCON_PLUGIN_TYPES_LOG_LEVEL_INFO, message); +} + +void falcon_warn(const char *message) { + logAt(FALCON_PLUGIN_TYPES_LOG_LEVEL_WARNING, message); +} + +void falcon_error(const char *message) { + logAt(FALCON_PLUGIN_TYPES_LOG_LEVEL_ERROR, message); +} + +void falcon_broadcast(const char *message) { + falcon_plugin_string_t text; + falcon_plugin_string_set(&text, message); + falcon_plugin_server_broadcast_message(&text); +} + +bool falcon_on(uint32_t event, falcon_event_handler handler) { + if (!grow((void **) &subscriptions, subscriptionCount, sizeof(*subscriptions))) + return false; + + const uint64_t id = falcon_plugin_events_subscribe(event, FALCON_PLUGIN_TYPES_PRIORITY_NORMAL, false); + if (id == 0) + return false; + + subscriptions[subscriptionCount].id = id; + subscriptions[subscriptionCount].handler = handler; + subscriptionCount++; + return true; +} + +bool falcon_command(const char *name, const char *description, falcon_command_handler handler) { + if (!grow((void **) &commands, commandCount, sizeof(*commands))) + return false; + + falcon_plugin_commands_command_descriptor_t descriptor; + falcon_plugin_string_set(&descriptor.name, name); + falcon_plugin_string_set(&descriptor.description, description); + falcon_plugin_string_set(&descriptor.usage, ""); + descriptor.permission = FALCON_PLUGIN_TYPES_COMMAND_PERMISSION_ANY; + descriptor.handler = commandCount; + if (!falcon_plugin_commands_register_command(&descriptor)) + return false; + + commands[commandCount++] = handler; + return true; +} + +void falcon_reply(const char *message) { + falcon_plugin_string_t text; + falcon_plugin_string_set(&text, message); + falcon_plugin_commands_sender_send_message(&text); +} + +bool falcon_schedule(uint64_t delay, uint64_t period, falcon_task_handler handler) { + if (!grow((void **) &tasks, taskCount, sizeof(*tasks))) + return false; + + if (falcon_plugin_scheduler_schedule_task(taskCount, delay, period) == 0) + return false; + + tasks[taskCount++] = handler; + return true; +} + +bool falcon_event_player(falcon_player *player) { + return falcon_plugin_event_event_player(player); +} + +bool falcon_sender_player(falcon_player *player) { + return falcon_plugin_commands_sender_player(player); +} + +void falcon_send(falcon_player player, const char *message) { + falcon_plugin_string_t text; + falcon_plugin_string_set(&text, message); + falcon_plugin_players_player_send_message(player, &text); +} + +const char *falcon_player_name(falcon_player player, char *buffer, uint32_t size) { + falcon_plugin_string_t name; + falcon_plugin_players_player_name(player, &name); + const size_t length = size == 0 ? 0 : (name.len < size - 1 ? name.len : size - 1); + if (size > 0) { + memcpy(buffer, name.ptr, length); + buffer[length] = '\0'; + } + falcon_plugin_string_free(&name); + return buffer; +} + +__attribute__((weak)) void falcon_disable(void) { +} + +void exports_falcon_plugin_plugin_on_load(void) { +} + +bool exports_falcon_plugin_plugin_on_enable(void) { + return falcon_enable(); +} + +void exports_falcon_plugin_plugin_on_disable(void) { + falcon_disable(); +} + +void exports_falcon_plugin_plugin_on_event(uint64_t subscription) { + for (uint32_t index = 0; index < subscriptionCount; index++) { + if (subscriptions[index].id == subscription) { + subscriptions[index].handler(); + return; + } + } +} + +bool exports_falcon_plugin_plugin_on_command(uint32_t handler, falcon_plugin_list_string_t *arguments) { + // binding strings are not nul terminated: copied into c strings for argv + const size_t count = arguments->len; + const char **argv = calloc(count + 1, sizeof(*argv)); + bool handled = false; + bool copied = argv != NULL; + for (size_t index = 0; copied && index < count; index++) { + char *argument = malloc(arguments->ptr[index].len + 1); + copied = argument != NULL; + if (copied) { + memcpy(argument, arguments->ptr[index].ptr, arguments->ptr[index].len); + argument[arguments->ptr[index].len] = '\0'; + argv[index] = argument; + } + } + + if (copied && handler < commandCount) + handled = commands[handler]((int) count, argv); + + for (size_t index = 0; argv != NULL && index < count; index++) + free((char *) argv[index]); + free(argv); + falcon_plugin_list_string_free(arguments); + return handled; +} + +void exports_falcon_plugin_plugin_on_task(uint64_t token) { + if (token < taskCount) + tasks[token](); +} + +void exports_falcon_plugin_plugin_on_service(falcon_plugin_string_t *name, falcon_plugin_string_t *request, + falcon_plugin_string_t *ret) { + falcon_plugin_string_free(name); + falcon_plugin_string_free(request); + falcon_plugin_string_dup(ret, ""); +} + +bool exports_falcon_plugin_plugin_on_item_use(uint32_t handler, exports_falcon_plugin_plugin_player_t player, + exports_falcon_plugin_plugin_borrow_item_t item) { + (void) handler; + (void) player; + (void) item; + return false; +} + +bool exports_falcon_plugin_plugin_on_item_use_on_block(uint32_t handler, exports_falcon_plugin_plugin_player_t player, + exports_falcon_plugin_plugin_borrow_item_t item, + exports_falcon_plugin_plugin_block_pos_t *position, + uint32_t face) { + (void) handler; + (void) player; + (void) item; + (void) position; + (void) face; + return false; +} + +bool exports_falcon_plugin_plugin_on_block_interact(uint32_t handler, exports_falcon_plugin_plugin_player_t player, + exports_falcon_plugin_plugin_block_pos_t *position, uint32_t face) { + (void) handler; + (void) player; + (void) position; + (void) face; + return false; +} + +void exports_falcon_plugin_plugin_on_block_break(uint32_t handler, exports_falcon_plugin_plugin_player_t player, + exports_falcon_plugin_plugin_block_pos_t *position) { + (void) handler; + (void) player; + (void) position; +} + +void exports_falcon_plugin_plugin_on_entity_tick(uint32_t handler, exports_falcon_plugin_plugin_entity_t entity) { + (void) handler; + (void) entity; +} + +bool exports_falcon_plugin_plugin_on_entity_interact(uint32_t handler, exports_falcon_plugin_plugin_entity_t entity, + exports_falcon_plugin_plugin_player_t player) { + (void) handler; + (void) entity; + (void) player; + return false; +} diff --git a/wasm/c/falcon_wasm.h b/wasm/c/falcon_wasm.h new file mode 100644 index 0000000..85439c2 --- /dev/null +++ b/wasm/c/falcon_wasm.h @@ -0,0 +1,130 @@ +#ifndef FALCON_WASM_H +#define FALCON_WASM_H + +/* plain callbacks over the wit-bindgen bindings; falcon_plugin.h stays usable for everything not wrapped */ + +#include "falcon_plugin.h" + +#include +#include + +typedef uint64_t falcon_player; + +typedef void (*falcon_event_handler)(void); + +typedef bool (*falcon_command_handler)(int argc, const char *const *argv); + +typedef void (*falcon_task_handler)(void); + +/* implemented by the plugin: false refuses to enable */ +bool falcon_enable(void); + +/* optional */ +void falcon_disable(void); + +void falcon_log(const char *message); + +void falcon_warn(const char *message); + +void falcon_error(const char *message); + +void falcon_broadcast(const char *message); + +/* type: a FALCON_EVENT_* value; false when refused */ +bool falcon_on(uint32_t event, falcon_event_handler handler); + +/* false when the name is taken */ +bool falcon_command(const char *name, const char *description, falcon_command_handler handler); + +void falcon_reply(const char *message); + +/* period 0: runs once after delay ticks */ +bool falcon_schedule(uint64_t delay, uint64_t period, falcon_task_handler handler); + +bool falcon_event_player(falcon_player *player); + +bool falcon_sender_player(falcon_player *player); + +void falcon_send(falcon_player player, const char *message); + +/* truncated to fit */ +const char *falcon_player_name(falcon_player player, char *buffer, uint32_t size); + +#define FALCON_EVENT_PLAYER_JOIN 1u +#define FALCON_EVENT_PLAYER_QUIT 2u +#define FALCON_EVENT_PLAYER_CHAT 3u +#define FALCON_EVENT_BLOCK_BREAK 4u +#define FALCON_EVENT_BLOCK_PLACE 5u +#define FALCON_EVENT_PLAYER_INTERACT_BLOCK 6u +#define FALCON_EVENT_ENTITY_DAMAGE 7u +#define FALCON_EVENT_ENTITY_DEATH 8u +#define FALCON_EVENT_PLAYER_DEATH 9u +#define FALCON_EVENT_PLAYER_RESPAWN 10u +#define FALCON_EVENT_PLAYER_MOVE 11u +#define FALCON_EVENT_PLAYER_DROP_ITEM 12u +#define FALCON_EVENT_PLAYER_PICKUP_ITEM 13u +#define FALCON_EVENT_DATA_PACKET_RECEIVE 14u +#define FALCON_EVENT_DATA_PACKET_SEND 15u +#define FALCON_EVENT_PLAYER_INTERACT_ENTITY 16u +#define FALCON_EVENT_INVENTORY_OPEN 17u +#define FALCON_EVENT_INVENTORY_CLOSE 18u +#define FALCON_EVENT_INVENTORY_TRANSACTION 19u +#define FALCON_EVENT_CRAFT_ITEM 20u +#define FALCON_EVENT_PLAYER_COMMAND 21u +#define FALCON_EVENT_SERVER_COMMAND 22u +#define FALCON_EVENT_SERVER_TICK 23u +#define FALCON_EVENT_PLAYER_GAME_MODE_CHANGE 24u +#define FALCON_EVENT_PLAYER_CHANGE_DIMENSION 25u +#define FALCON_EVENT_PROJECTILE_HIT 26u +#define FALCON_EVENT_EXPLOSION 27u +#define FALCON_EVENT_FIRE_SPREAD 28u +#define FALCON_EVENT_BLOCK_BURN 29u +#define FALCON_EVENT_CUSTOM 30u +#define FALCON_EVENT_PLAYER_PRE_LOGIN 31u +#define FALCON_EVENT_PLAYER_TOGGLE_SNEAK 32u +#define FALCON_EVENT_PLAYER_TOGGLE_SPRINT 33u +#define FALCON_EVENT_PLAYER_TOGGLE_FLIGHT 34u +#define FALCON_EVENT_PLAYER_ITEM_HELD 35u +#define FALCON_EVENT_PLAYER_ITEM_CONSUME 36u +#define FALCON_EVENT_PLAYER_BED_ENTER 37u +#define FALCON_EVENT_PLAYER_BED_LEAVE 38u +#define FALCON_EVENT_PLAYER_JUMP 39u +#define FALCON_EVENT_PLAYER_FOOD_CHANGE 40u +#define FALCON_EVENT_PLAYER_EXPERIENCE_CHANGE 41u +#define FALCON_EVENT_ENTITY_SPAWN 42u +#define FALCON_EVENT_ENTITY_TRANSFORM 43u +#define FALCON_EVENT_ENTITY_TARGET 44u +#define FALCON_EVENT_PROJECTILE_LAUNCH 45u +#define FALCON_EVENT_BLOCK_GROW 46u +#define FALCON_EVENT_BLOCK_SPREAD 47u +#define FALCON_EVENT_BLOCK_FORM 48u +#define FALCON_EVENT_BLOCK_FADE 49u +#define FALCON_EVENT_LIQUID_FLOW 50u +#define FALCON_EVENT_PISTON_EXTEND 51u +#define FALCON_EVENT_PISTON_RETRACT 52u +#define FALCON_EVENT_REDSTONE_CHANGE 53u +#define FALCON_EVENT_SIGN_CHANGE 54u +#define FALCON_EVENT_LEAVES_DECAY 55u +#define FALCON_EVENT_WEATHER_CHANGE 56u +#define FALCON_EVENT_THUNDER_CHANGE 57u +#define FALCON_EVENT_CHUNK_LOAD 58u +#define FALCON_EVENT_CHUNK_UNLOAD 59u +#define FALCON_EVENT_FURNACE_SMELT 60u +#define FALCON_EVENT_FURNACE_BURN 61u +#define FALCON_EVENT_ITEM_ENCHANT 62u +#define FALCON_EVENT_ITEM_DAMAGE 63u +#define FALCON_EVENT_ITEM_BREAK 64u +#define FALCON_EVENT_WORLD_LOAD 65u +#define FALCON_EVENT_WORLD_UNLOAD 66u +#define FALCON_EVENT_PLAYER_CHANGE_WORLD 67u +#define FALCON_EVENT_CONNECTION_OPEN 68u +#define FALCON_EVENT_PLAYER_FORM_RESPONSE 69u +#define FALCON_GAME_MODE_SURVIVAL 0u +#define FALCON_GAME_MODE_CREATIVE 1u +#define FALCON_GAME_MODE_ADVENTURE 2u +#define FALCON_GAME_MODE_SPECTATOR 6u +#define FALCON_DIMENSION_OVERWORLD 0u +#define FALCON_DIMENSION_NETHER 1u +#define FALCON_DIMENSION_THE_END 2u + +#endif diff --git a/wasm/falcon.wit b/wasm/falcon.wit new file mode 100644 index 0000000..dfe833d --- /dev/null +++ b/wasm/falcon.wit @@ -0,0 +1,421 @@ +/// The Falcon plugin API for WebAssembly plugins. +/// +/// Every imported function mirrors one entry of the C API table in `falcon_api.h` (API 1.3.3) and calls it, +/// so a Wasm plugin reaches exactly what a native plugin reaches. The only C entry without a counterpart is +/// `runAsync`: a plugin instance is not thread safe, so it cannot run on the scheduler's worker thread. +/// +/// Handles are never pointers. Players and entities are their runtime id and are looked up on every call, so +/// a stale id is simply not found. Levels and items are host-side handles that are checked on every call. +/// Events and command senders have no handle: `event` and the sender functions of `commands` act on the +/// event or command being handled and return default values outside a handler. +package falcon:plugin@1.3.3; + +interface types { + /// The runtime id of a player. A player is also an entity, so the same id works with `entities`. + type player = u64; + + /// The runtime id of an entity. + type entity = u64; + + /// One of the `FALCON_DIMENSION_*` values. + type dimension = u32; + + /// One of the `FALCON_GAME_MODE_*` values. + type game-mode = u32; + + /// One of the `FALCON_EVENT_*` values. + type event-kind = u32; + + record vec3 { + x: f64, + y: f64, + z: f64, + } + + record block-pos { + x: s32, + y: s32, + z: s32, + } + + /// `FALCON_LOG_*`, in order. + enum log-level { + info, + warning, + error, + } + + /// `FALCON_PRIORITY_*`, in order. + enum priority { + lowest, + low, + normal, + high, + highest, + monitor, + } + + /// `FALCON_PERMISSION_DEFAULT_FALSE`, `_TRUE` and `_OPERATOR`, in order. + enum permission-default { + deny, + allow, + operator, + } + + /// `FALCON_PERMISSION_ANY` and `FALCON_PERMISSION_OPERATOR`, in order. + enum command-permission { + any, + operator, + } + + /// `FALCON_ARMOR_*`, in order. + enum armor-slot { + head, + chest, + legs, + feet, + } + + /// A level of a loaded world. A level whose world was unloaded behaves like a missing one. + resource level; + + /// An item stack. Items returned by `item-create`, `item-copy` and the inventory getters are copies owned + /// by the plugin. Items returned by `event-item` and `event-result`, and the item passed to a custom item + /// handler, are the server's own stack and stop working when the handler returns. + resource item; +} + +interface server { + use types.{player, log-level}; + + server-version: func() -> string; + log: func(level: log-level, message: string); + plugin-name: func() -> string; + plugin-data-folder: func() -> string; + online-player-count: func() -> u32; + online-player: func(index: u32) -> option; + find-player: func(name: string) -> option; + broadcast-message: func(message: string); +} + +interface players { + use types.{player, entity, game-mode}; + + player-name: func(player: player) -> string; + player-is-operator: func(player: player) -> bool; + player-send-message: func(player: player, message: string); + player-kick: func(player: player, reason: string); + player-entity: func(player: player) -> option; + entity-player: func(entity: entity) -> option; + player-game-mode: func(player: player) -> game-mode; + player-set-game-mode: func(player: player, game-mode: game-mode); + player-xuid: func(player: player) -> string; + player-uuid: func(player: player) -> string; + player-address: func(player: player) -> string; + player-send-title: func(player: player, title: string, subtitle: string); + player-send-action-bar: func(player: player, message: string); + player-food: func(player: player) -> f32; + player-set-food: func(player: player, food: f32); + player-xp-level: func(player: player) -> s32; + player-set-xp-level: func(player: player, level: s32); + player-set-operator: func(player: player, operator: bool); + player-send-form: func(player: player, form-json: string) -> u32; + player-transfer: func(player: player, address: string, port: u32) -> bool; + player-protocol-version: func(player: player) -> s32; + player-send-packet: func(player: player, packet-id: u32, data: list) -> bool; +} + +interface entities { + use types.{entity, level, vec3}; + + entity-type: func(entity: entity) -> string; + entity-runtime-id: func(entity: entity) -> u64; + entity-level: func(entity: entity) -> option; + entity-position: func(entity: entity) -> vec3; + entity-rotation: func(entity: entity) -> vec3; + entity-teleport: func(entity: entity, level: option>, position: vec3); + entity-motion: func(entity: entity) -> vec3; + entity-set-motion: func(entity: entity, motion: vec3); + entity-health: func(entity: entity) -> f32; + entity-max-health: func(entity: entity) -> f32; + entity-set-health: func(entity: entity, health: f32); + entity-is-alive: func(entity: entity) -> bool; + entity-damage: func(entity: entity, amount: f32, cause: option, attacker: option) -> bool; + entity-kill: func(entity: entity); + entity-remove: func(entity: entity); + entity-name-tag: func(entity: entity) -> string; + entity-set-name-tag: func(entity: entity, name-tag: string); + entity-is-on-fire: func(entity: entity) -> bool; + entity-set-on-fire: func(entity: entity, ticks: u32); + entity-is-invisible: func(entity: entity) -> bool; + entity-set-invisible: func(entity: entity, invisible: bool); + level-entity-count: func(level: borrow) -> u32; + level-entity: func(level: borrow, index: u32) -> option; +} + +interface levels { + use types.{entity, dimension, level, item, vec3, block-pos}; + + server-level: func(dimension: dimension) -> option; + level-dimension: func(level: borrow) -> dimension; + level-name: func(level: borrow) -> string; + level-get-block: func(level: borrow, position: block-pos) -> string; + level-get-block-states: func(level: borrow, position: block-pos) -> string; + level-set-block: func(level: borrow, position: block-pos, name: string, states-json: option) -> bool; + level-break-block: func(level: borrow, position: block-pos, drop-items: bool) -> bool; + level-is-chunk-loaded: func(level: borrow, chunk-x: s32, chunk-z: s32) -> bool; + level-highest-block-y: func(level: borrow, x: s32, z: s32) -> s32; + level-time: func(level: borrow) -> s64; + level-set-time: func(level: borrow, time: s64); + level-is-raining: func(level: borrow) -> bool; + level-set-raining: func(level: borrow, raining: bool); + level-is-thundering: func(level: borrow) -> bool; + level-set-thundering: func(level: borrow, thundering: bool); + level-spawn-position: func(level: borrow) -> vec3; + level-spawn-entity: func(level: borrow, identifier: string, position: vec3) -> option; + level-drop-item: func(level: borrow, position: vec3, item: borrow); + level-strike-lightning: func(level: borrow, position: vec3); + level-create-explosion: func(level: borrow, position: vec3, power: f32, break-blocks: bool); + server-world-count: func() -> u32; + server-world-name: func(index: u32) -> option; + server-default-world-name: func() -> string; + world-level: func(name: string, dimension: dimension) -> option; + server-load-world: func(name: string, create: bool, seed: option) -> bool; + server-unload-world: func(name: string) -> bool; +} + +interface items { + use types.{player, item, armor-slot}; + + item-create: func(identifier: string, count: u32) -> option; + item-copy: func(item: borrow) -> option; + item-is-empty: func(item: borrow) -> bool; + item-identifier: func(item: borrow) -> string; + item-count: func(item: borrow) -> u32; + item-set-count: func(item: borrow, count: u32); + item-damage: func(item: borrow) -> s32; + item-set-damage: func(item: borrow, damage: s32); + item-custom-name: func(item: borrow) -> option; + item-set-custom-name: func(item: borrow, name: option); + item-lore-count: func(item: borrow) -> u32; + item-lore: func(item: borrow, index: u32) -> option; + item-set-lore: func(item: borrow, lines: list); + item-enchantment-level: func(item: borrow, enchantment: u32) -> s32; + item-set-enchantment-level: func(item: borrow, enchantment: u32, level: s32); + player-inventory-size: func(player: player) -> u32; + player-inventory-item: func(player: player, slot: u32) -> option; + player-set-inventory-item: func(player: player, slot: u32, item: option>); + player-give-item: func(player: player, item: borrow) -> bool; + player-selected-slot: func(player: player) -> u32; + player-set-selected-slot: func(player: player, slot: u32); + player-armor-item: func(player: player, slot: armor-slot) -> option; + player-set-armor-item: func(player: player, slot: armor-slot, item: option>); + player-offhand-item: func(player: player) -> option; + player-set-offhand-item: func(player: player, item: option>); + player-clear-inventory: func(player: player); +} + +interface events { + use types.{event-kind, priority}; + + record custom-event-result { + data: option, + cancelled: bool, + } + + /// Returns the subscription id, or 0 when the subscription was refused. `on-event` receives this id. + subscribe: func(kind: event-kind, priority: priority, ignore-cancelled: bool) -> u64; + subscribe-custom-event: func(name: string, priority: priority, ignore-cancelled: bool) -> u64; + /// Only removes subscriptions of this plugin. + unsubscribe: func(subscription: u64); + fire-custom-event: func(name: string, data: string, cancellable: bool) -> custom-event-result; +} + +/// The event being handled by `on-event`. +interface event { + use types.{player, entity, level, item, event-kind, game-mode, dimension, vec3, block-pos}; + + event-type: func() -> event-kind; + event-is-cancellable: func() -> bool; + event-is-cancelled: func() -> bool; + event-set-cancelled: func(cancelled: bool); + event-player: func() -> option; + event-message: func() -> option; + event-set-message: func(message: string); + event-entity: func() -> option; + event-attacker: func() -> option; + event-block-position: func() -> block-pos; + event-block-face: func() -> u32; + event-block-name: func() -> string; + event-amount: func() -> f64; + event-set-amount: func(amount: f64); + event-cause: func() -> string; + event-from: func() -> vec3; + event-to: func() -> vec3; + event-set-to: func(position: vec3); + event-item: func() -> option; + event-packet-id: func() -> u32; + event-packet-data: func() -> list; + event-set-packet-data: func(data: list); + event-level: func() -> option; + event-target: func() -> option; + event-position: func() -> vec3; + event-block-count: func() -> u32; + event-block-at: func(index: u32) -> block-pos; + event-set-blocks: func(positions: list); + event-game-mode: func() -> game-mode; + event-previous-game-mode: func() -> game-mode; + event-dimension: func() -> dimension; + event-previous-dimension: func() -> dimension; + event-tick: func() -> u64; + event-source-container: func() -> string; + event-source-slot: func() -> s32; + event-destination-container: func() -> string; + event-destination-slot: func() -> s32; + event-name: func() -> string; + event-data: func() -> string; + event-set-data: func(data: string); + event-state: func() -> bool; + event-previous-amount: func() -> f64; + event-previous-block-name: func() -> string; + event-chunk-x: func() -> s32; + event-chunk-z: func() -> s32; + event-result: func() -> option; + event-set-result: func(item: borrow); + event-world-name: func() -> string; + event-previous-world-name: func() -> string; + event-address: func() -> string; + event-transport: func() -> string; + event-client-guid: func() -> u64; + event-mtu-size: func() -> u32; + event-client-data: func() -> string; + event-client-data-field: func(key: string) -> string; + event-form-id: func() -> u32; + event-form-response: func() -> string; +} + +/// Command registration, and the sender of the command being handled by `on-command`. +interface commands { + use types.{player, command-permission}; + + record command-descriptor { + name: string, + description: string, + usage: string, + permission: command-permission, + /// Passed back to `on-command`. + handler: u32, + } + + register-command: func(descriptor: command-descriptor) -> bool; + sender-name: func() -> string; + sender-player: func() -> option; + sender-send-message: func(message: string); + sender-has-permission: func(node: string) -> bool; +} + +interface scheduler { + /// `token` is passed back to `on-task`. Returns the task id, or 0 when the task was refused. + schedule-task: func(token: u64, delay-ticks: u64, period-ticks: u64) -> u64; + /// Only cancels tasks of this plugin. + cancel-task: func(task: u64); +} + +interface permissions { + use types.{player, permission-default}; + + register-permission: func(node: string, default-value: permission-default) -> bool; + player-has-permission: func(player: player, node: string) -> bool; + player-set-permission: func(player: player, node: string, value: bool); + player-unset-permission: func(player: player, node: string); +} + +interface services { + /// `on-service` is called with `name` when a plugin calls the service. + register-service: func(name: string) -> bool; + unregister-service: func(name: string); + has-service: func(name: string) -> bool; + service-provider: func(name: string) -> option; + call-service: func(name: string, request: string) -> option; +} + +/// 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 { + record custom-item { + identifier: string, + display-name: string, + icon: string, + creative-category: string, + max-stack-size: u32, + max-durability: u32, + hand-equipped: bool, + on-use: option, + on-use-on-block: option, + } + + record custom-block { + identifier: string, + display-name: string, + texture: string, + creative-category: string, + destroy-time: f32, + explosion-resistance: f32, + light-emission: u32, + friction: f32, + drop: option, + on-interact: option, + on-break: option, + } + + record custom-entity { + identifier: string, + width: f32, + height: f32, + max-health: f32, + summonable: bool, + on-tick: option, + on-interact: option, + } + + register-custom-item: func(descriptor: custom-item) -> bool; + register-custom-block: func(descriptor: custom-block) -> bool; + register-custom-entity: func(descriptor: custom-entity) -> bool; +} + +/// The functions the server calls: `FalconPluginCallbacks` and the C handler callbacks. +interface plugin { + use types.{player, entity, item, block-pos}; + + on-load: func(); + on-enable: func() -> bool; + on-disable: func(); + on-event: func(subscription: u64); + on-command: func(handler: u32, arguments: list) -> bool; + on-task: func(token: u64); + on-service: func(name: string, request: string) -> string; + on-item-use: func(handler: u32, player: player, item: borrow) -> bool; + on-item-use-on-block: func(handler: u32, player: player, item: borrow, position: block-pos, face: u32) -> bool; + on-block-interact: func(handler: u32, player: player, position: block-pos, face: u32) -> bool; + on-block-break: func(handler: u32, player: player, position: block-pos); + on-entity-tick: func(handler: u32, entity: entity); + on-entity-interact: func(handler: u32, entity: entity, player: player) -> bool; +} + +world falcon-plugin { + import types; + import server; + import players; + import entities; + import levels; + import items; + import events; + import event; + import commands; + import scheduler; + import permissions; + import services; + import content; + + export plugin; +}