From a7f6c9d2dd531afa30ad83f6fd3b10431de2ebdd Mon Sep 17 00:00:00 2001 From: NaySurGithub <96527285+NaySurGithub@users.noreply.github.com> Date: Sat, 14 Nov 2026 21:23:05 +0100 Subject: [PATCH] feat: add world containers, signs and the ender chest to the plugin api --- README.md | 21 +++++++ java/src/main/java/falcon/api/Item.java | 10 ++++ java/src/main/java/falcon/api/Level.java | 56 +++++++++++++++++++ .../main/java/falcon/api/PlayerInventory.java | 19 +++++++ java/src/main/java/falcon/api/SignSide.java | 18 ++++++ native/include/falcon/Level.hpp | 39 +++++++++++++ native/include/falcon/PlayerInventory.hpp | 20 +++++++ native/include/falcon/falcon_api.h | 13 +++++ wasm/falcon.wit | 12 ++++ 9 files changed, 208 insertions(+) create mode 100644 java/src/main/java/falcon/api/SignSide.java diff --git a/README.md b/README.md index 19ce4b9..93f4af5 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,27 @@ 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: + +```cpp +const falcon::BlockPos chest{10, 64, 10}; +for (uint32_t slot = 0; slot < level.containerSize(chest); ++slot) { + if (std::optional 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. + ### C# plugins The `dotnet` runtime is deprecated. Write new plugins for Wasm, native C++ or Java. diff --git a/java/src/main/java/falcon/api/Item.java b/java/src/main/java/falcon/api/Item.java index 198a873..33c770a 100644 --- a/java/src/main/java/falcon/api/Item.java +++ b/java/src/main/java/falcon/api/Item.java @@ -36,6 +36,16 @@ public static Optional adopt(MemorySegment handle) { return Optional.of(new Item(handle, true)); } + // an empty stack is freed at once: nobody would close it + static Optional adoptFilled(MemorySegment handle) { + Optional item = adopt(handle); + if (item.isPresent() && item.get().isEmpty()) { + item.get().close(); + return Optional.empty(); + } + return item; + } + public static Item borrow(MemorySegment handle) { return new Item(handle == null ? MemorySegment.NULL : handle, false); } diff --git a/java/src/main/java/falcon/api/Level.java b/java/src/main/java/falcon/api/Level.java index 3e713fe..68da5ac 100644 --- a/java/src/main/java/falcon/api/Level.java +++ b/java/src/main/java/falcon/api/Level.java @@ -159,6 +159,62 @@ public void spawnParticle(String particle, Vec3 position) { } } + /** + * The slot count of the chest, barrel, furnace, hopper or other container block there; 0 for none. + */ + public int containerSize(BlockPos position) { + try (Arena arena = Arena.ofConfined()) { + return api().levelContainerSize(mHandle, Interop.blockPos(arena, position)); + } + } + + /** + * A copy of the item in a container slot; empty for an empty slot or no container. + */ + public Optional containerItem(BlockPos position, int slot) { + try (Arena arena = Arena.ofConfined()) { + return Item.adoptFilled(api().levelContainerItem(mHandle, Interop.blockPos(arena, position), slot)); + } + } + + public boolean setContainerItem(BlockPos position, int slot, Item item) { + try (Arena arena = Arena.ofConfined()) { + return api().levelSetContainerItem(mHandle, Interop.blockPos(arena, position), slot, item.handle()) != 0; + } + } + + public boolean clearContainerItem(BlockPos position, int slot) { + try (Arena arena = Arena.ofConfined()) { + return api().levelSetContainerItem(mHandle, Interop.blockPos(arena, position), slot, + MemorySegment.NULL) != 0; + } + } + + public Optional signText(BlockPos position) { + return signText(position, SignSide.FRONT); + } + + /** + * The text of a sign or hanging sign, raw or JSON as the game saved it; empty when no sign is there. + */ + public Optional signText(BlockPos position, SignSide side) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment text = api().levelSignText(mHandle, Interop.blockPos(arena, position), side.value()); + return Interop.isNull(text) ? Optional.empty() : Optional.of(Interop.string(text)); + } + } + + public boolean setSignText(BlockPos position, String text) { + return setSignText(position, text, SignSide.FRONT); + } + + public boolean setSignText(BlockPos position, String text, SignSide side) { + try (Arena arena = Arena.ofConfined()) { + return api().levelSetSignText(mHandle, Interop.blockPos(arena, position), side.value(), + Interop.text(arena, text)) != 0; + } + } + public List entities() { int count = api().levelEntityCount(mHandle); List result = new ArrayList<>(count); diff --git a/java/src/main/java/falcon/api/PlayerInventory.java b/java/src/main/java/falcon/api/PlayerInventory.java index 62d9908..0016805 100644 --- a/java/src/main/java/falcon/api/PlayerInventory.java +++ b/java/src/main/java/falcon/api/PlayerInventory.java @@ -29,6 +29,25 @@ public boolean give(Item item) { return api().playerGiveItem(mPlayer, item.handle()) != 0; } + public int enderChestSize() { + return api().playerEnderChestSize(mPlayer); + } + + /** + * The player's own ender chest, the same in every ender chest block and saved with the player. + */ + public Optional enderChestItem(int slot) { + return Item.adoptFilled(api().playerEnderChestItem(mPlayer, slot)); + } + + public boolean setEnderChestItem(int slot, Item item) { + return api().playerSetEnderChestItem(mPlayer, slot, item.handle()) != 0; + } + + public boolean clearEnderChestItem(int slot) { + return api().playerSetEnderChestItem(mPlayer, slot, MemorySegment.NULL) != 0; + } + public int selectedSlot() { return api().playerSelectedSlot(mPlayer); } diff --git a/java/src/main/java/falcon/api/SignSide.java b/java/src/main/java/falcon/api/SignSide.java new file mode 100644 index 0000000..7e6905e --- /dev/null +++ b/java/src/main/java/falcon/api/SignSide.java @@ -0,0 +1,18 @@ +package falcon.api; + +import falcon.api.internal.FalconAbi; + +public enum SignSide { + FRONT(FalconAbi.SIGN_FRONT), + BACK(FalconAbi.SIGN_BACK); + + private final int mValue; + + SignSide(int value) { + mValue = value; + } + + public int value() { + return mValue; + } +} diff --git a/native/include/falcon/Level.hpp b/native/include/falcon/Level.hpp index 36a03ff..5ff4594 100644 --- a/native/include/falcon/Level.hpp +++ b/native/include/falcon/Level.hpp @@ -14,6 +14,11 @@ namespace falcon { class Entity; + enum class SignSide : uint32_t { + Front = FALCON_SIGN_FRONT, + Back = FALCON_SIGN_BACK + }; + enum class Dimension : uint32_t { Overworld = FALCON_DIMENSION_OVERWORLD, Nether = FALCON_DIMENSION_NETHER, @@ -117,6 +122,40 @@ namespace falcon { detail::api().levelSpawnParticle(mHandle, particle.c_str(), position.raw()); } + /** The slot count of the chest, barrel, furnace, hopper or other container block there; 0 for none. */ + uint32_t containerSize(const BlockPos &position) const { + return detail::api().levelContainerSize(mHandle, position.raw()); + } + + /** A copy of the item in a container slot; nothing for an empty slot or no container. */ + std::optional containerItem(const BlockPos &position, uint32_t slot) const { + std::optional found = Item::adopt(detail::api().levelContainerItem(mHandle, position.raw(), slot)); + if (found.has_value() && found->isEmpty()) + return std::nullopt; + return found; + } + + bool setContainerItem(const BlockPos &position, uint32_t slot, const Item &item) const { + return detail::api().levelSetContainerItem(mHandle, position.raw(), slot, item.handle()) != 0; + } + + bool clearContainerItem(const BlockPos &position, uint32_t slot) const { + return detail::api().levelSetContainerItem(mHandle, position.raw(), slot, nullptr) != 0; + } + + /** The text of a sign or hanging sign, raw or JSON as the game saved it; nothing when no sign is there. */ + std::optional signText(const BlockPos &position, SignSide side = SignSide::Front) const { + const char *text = detail::api().levelSignText(mHandle, position.raw(), static_cast(side)); + if (text == nullptr) + return std::nullopt; + return std::string(text); + } + + bool setSignText(const BlockPos &position, const std::string &text, SignSide side = SignSide::Front) const { + return detail::api().levelSetSignText(mHandle, position.raw(), static_cast(side), + text.c_str()) != 0; + } + std::vector entities() const; FalconLevel *handle() const { diff --git a/native/include/falcon/PlayerInventory.hpp b/native/include/falcon/PlayerInventory.hpp index 0bfb198..d70c3d9 100644 --- a/native/include/falcon/PlayerInventory.hpp +++ b/native/include/falcon/PlayerInventory.hpp @@ -35,6 +35,26 @@ namespace falcon { return detail::api().playerGiveItem(mPlayer, item.handle()) != 0; } + uint32_t enderChestSize() const { + return detail::api().playerEnderChestSize(mPlayer); + } + + /** The player's own ender chest, the same in every ender chest block and saved with the player. */ + std::optional enderChestItem(uint32_t slot) const { + std::optional found = Item::adopt(detail::api().playerEnderChestItem(mPlayer, slot)); + if (found.has_value() && found->isEmpty()) + return std::nullopt; + return found; + } + + bool setEnderChestItem(uint32_t slot, const Item &item) const { + return detail::api().playerSetEnderChestItem(mPlayer, slot, item.handle()) != 0; + } + + bool clearEnderChestItem(uint32_t slot) const { + return detail::api().playerSetEnderChestItem(mPlayer, slot, nullptr) != 0; + } + uint32_t selectedSlot() const { return detail::api().playerSelectedSlot(mPlayer); } diff --git a/native/include/falcon/falcon_api.h b/native/include/falcon/falcon_api.h index 69230e7..772d653 100644 --- a/native/include/falcon/falcon_api.h +++ b/native/include/falcon/falcon_api.h @@ -188,6 +188,10 @@ typedef uint32_t FalconDisplaySlot; #define FALCON_DISPLAY_SLOT_LIST 1u #define FALCON_DISPLAY_SLOT_BELOW_NAME 2u +typedef uint32_t FalconSignSide; +#define FALCON_SIGN_FRONT 0u +#define FALCON_SIGN_BACK 1u + typedef uint32_t FalconLogLevel; #define FALCON_LOG_INFO 0u #define FALCON_LOG_WARNING 1u @@ -552,6 +556,15 @@ typedef struct FalconServerApi { FalconEntity *(*scoreboardScoreEntity)(const char *objective, uint32_t index); int (*scoreboardSetDisplay)(FalconDisplaySlot slot, const char *objective, int ascending); const char *(*scoreboardDisplayObjective)(FalconDisplaySlot slot); + + uint32_t (*levelContainerSize)(FalconLevel *level, FalconBlockPos position); + FalconItem *(*levelContainerItem)(FalconLevel *level, FalconBlockPos position, uint32_t slot); + int (*levelSetContainerItem)(FalconLevel *level, FalconBlockPos position, uint32_t slot, FalconItem *item); + const char *(*levelSignText)(FalconLevel *level, FalconBlockPos position, FalconSignSide side); + int (*levelSetSignText)(FalconLevel *level, FalconBlockPos position, FalconSignSide side, const char *text); + uint32_t (*playerEnderChestSize)(FalconPlayer *player); + FalconItem *(*playerEnderChestItem)(FalconPlayer *player, uint32_t slot); + int (*playerSetEnderChestItem)(FalconPlayer *player, uint32_t slot, FalconItem *item); } FalconServerApi; typedef int (*FalconPluginEntry)(const FalconServerApi *api, FalconPlugin *plugin, FalconPluginCallbacks *callbacks); diff --git a/wasm/falcon.wit b/wasm/falcon.wit index f0c3258..0e4fa88 100644 --- a/wasm/falcon.wit +++ b/wasm/falcon.wit @@ -221,6 +221,15 @@ interface levels { server-unload-world: func(name: string) -> bool; level-play-sound: func(level: borrow, sound: string, position: vec3, volume: f32, pitch: f32); level-spawn-particle: func(level: borrow, particle: string, position: vec3); + level-container-size: func(level: borrow, position: block-pos) -> u32; + /// A copy of the item in the slot; `none` for an empty slot or no container. + level-container-item: func(level: borrow, position: block-pos, slot: u32) -> option; + /// `none` empties the slot. + level-set-container-item: func(level: borrow, position: block-pos, slot: u32, item: option>) + -> bool; + /// `side`: `FALCON_SIGN_FRONT` or `FALCON_SIGN_BACK`. + level-sign-text: func(level: borrow, position: block-pos, side: u32) -> option; + level-set-sign-text: func(level: borrow, position: block-pos, side: u32, text: string) -> bool; } /// The server's scoreboard. A participant is an entity, a player through its entity id, or a fake player by name. @@ -283,6 +292,9 @@ interface items { player-offhand-item: func(player: player) -> option; player-set-offhand-item: func(player: player, item: option>); player-clear-inventory: func(player: player); + player-ender-chest-size: func(player: player) -> u32; + player-ender-chest-item: func(player: player, slot: u32) -> option; + player-set-ender-chest-item: func(player: player, slot: u32, item: option>) -> bool; } interface events {