diff --git a/README.md b/README.md index 93f4af5..f4543d3 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,27 @@ 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: + +```cpp +player.entity().setData("rank", "builder"); +std::optional rank = player.entity().data("rank"); + +falcon::PlayerInventory inventory = player.inventory(); +if (std::optional 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, and `Entity::nbt` returns what the server would save +for an entity. + ### 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/Entity.java b/java/src/main/java/falcon/api/Entity.java index 8b04dcb..ea828f1 100644 --- a/java/src/main/java/falcon/api/Entity.java +++ b/java/src/main/java/falcon/api/Entity.java @@ -7,6 +7,7 @@ import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -225,6 +226,21 @@ public boolean setAttribute(String name, float value) { } } + /** + * What the server would save for this entity, as little-endian NBT; read only. + */ + public byte[] nbt() { + try (Arena arena = Arena.ofConfined()) { + MemorySegment length = arena.allocate(ValueLayout.JAVA_INT); + MemorySegment data = api().entityNbt(mHandle, length); + int size = length.get(ValueLayout.JAVA_INT, 0); + if (Interop.isNull(data) || size <= 0) { + return new byte[0]; + } + return data.reinterpret(size).toArray(ValueLayout.JAVA_BYTE); + } + } + public Optional asPlayer() { MemorySegment player = api().entityPlayer(mHandle); if (Interop.isNull(player)) { diff --git a/java/src/main/java/falcon/api/Item.java b/java/src/main/java/falcon/api/Item.java index 33c770a..e20209e 100644 --- a/java/src/main/java/falcon/api/Item.java +++ b/java/src/main/java/falcon/api/Item.java @@ -50,6 +50,33 @@ public static Item borrow(MemorySegment handle) { return new Item(handle == null ? MemorySegment.NULL : handle, false); } + /** + * The item's tag as little-endian NBT, the game's save format; empty for an item without one. + */ + public byte[] nbt() { + try (Arena arena = Arena.ofConfined()) { + MemorySegment length = arena.allocate(ValueLayout.JAVA_INT); + MemorySegment data = api().itemNbt(mHandle, length); + int size = length.get(ValueLayout.JAVA_INT, 0); + if (Interop.isNull(data) || size <= 0) { + return new byte[0]; + } + return data.reinterpret(size).toArray(ValueLayout.JAVA_BYTE); + } + } + + /** + * Replaces the item's tag; empty data removes it. Returns false when the data is not a little-endian NBT + * compound. + */ + public boolean setNbt(byte[] data) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment bytes = data.length == 0 ? MemorySegment.NULL : arena.allocateFrom(ValueLayout.JAVA_BYTE, + data); + return api().itemSetNbt(mHandle, bytes, data.length) != 0; + } + } + public Optional copy() { if (Interop.isNull(mHandle)) { return Optional.empty(); diff --git a/java/src/main/java/falcon/api/PersistentData.java b/java/src/main/java/falcon/api/PersistentData.java new file mode 100644 index 0000000..7596cff --- /dev/null +++ b/java/src/main/java/falcon/api/PersistentData.java @@ -0,0 +1,90 @@ +package falcon.api; + +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.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Text this plugin keeps on entities, players and items, saved with them. Keys belong to the plugin: another plugin + * never sees them. Entities without saved state, such as dropped items, keep nothing, and a copy of an inventory + * item must be put back for its change to stay. + */ +public final class PersistentData { + private final PluginContext mContext; + + PersistentData(PluginContext context) { + mContext = context; + } + + public Optional get(Entity entity, String key) { + try (Arena arena = Arena.ofConfined()) { + return optional(api().entityData(mContext.handle(), entity.handle(), Interop.text(arena, key))); + } + } + + public boolean set(Entity entity, String key, String value) { + try (Arena arena = Arena.ofConfined()) { + return api().entitySetData(mContext.handle(), entity.handle(), Interop.text(arena, key), + Interop.text(arena, value)) != 0; + } + } + + public boolean remove(Entity entity, String key) { + try (Arena arena = Arena.ofConfined()) { + return api().entitySetData(mContext.handle(), entity.handle(), Interop.text(arena, key), + MemorySegment.NULL) != 0; + } + } + + public List keys(Entity entity) { + int count = api().entityDataCount(mContext.handle(), entity.handle()); + List keys = new ArrayList<>(Math.max(count, 0)); + for (int index = 0; index < count; index++) { + keys.add(Interop.string(api().entityDataKey(mContext.handle(), entity.handle(), index))); + } + return keys; + } + + public Optional get(Item item, String key) { + try (Arena arena = Arena.ofConfined()) { + return optional(api().itemData(mContext.handle(), item.handle(), Interop.text(arena, key))); + } + } + + public boolean set(Item item, String key, String value) { + try (Arena arena = Arena.ofConfined()) { + return api().itemSetData(mContext.handle(), item.handle(), Interop.text(arena, key), + Interop.text(arena, value)) != 0; + } + } + + public boolean remove(Item item, String key) { + try (Arena arena = Arena.ofConfined()) { + return api().itemSetData(mContext.handle(), item.handle(), Interop.text(arena, key), + MemorySegment.NULL) != 0; + } + } + + public List keys(Item item) { + int count = api().itemDataCount(mContext.handle(), item.handle()); + List keys = new ArrayList<>(Math.max(count, 0)); + for (int index = 0; index < count; index++) { + keys.add(Interop.string(api().itemDataKey(mContext.handle(), item.handle(), index))); + } + return keys; + } + + private static Optional optional(MemorySegment value) { + return Interop.isNull(value) ? Optional.empty() : Optional.of(Interop.string(value)); + } + + private static NativeApi api() { + return Interop.api(); + } +} diff --git a/java/src/main/java/falcon/api/Plugin.java b/java/src/main/java/falcon/api/Plugin.java index 06f6085..2cff129 100644 --- a/java/src/main/java/falcon/api/Plugin.java +++ b/java/src/main/java/falcon/api/Plugin.java @@ -20,6 +20,7 @@ public abstract class Plugin { private final Permissions mPermissions; private final Content mContent; private final Services mServices; + private final PersistentData mData; private Config mConfig; protected Plugin() { @@ -32,6 +33,7 @@ protected Plugin() { mPermissions = new Permissions(mContext); mContent = new Content(mContext); mServices = new Services(mContext); + mData = new PersistentData(mContext); } public void onLoad() { @@ -84,6 +86,10 @@ public final Services services() { return mServices; } + public final PersistentData data() { + return mData; + } + public final Config config() { if (mConfig == null) { mConfig = new Config(dataFolder().resolve("config.yml")); diff --git a/native/include/falcon/Entity.hpp b/native/include/falcon/Entity.hpp index e9430cb..f49cee4 100644 --- a/native/include/falcon/Entity.hpp +++ b/native/include/falcon/Entity.hpp @@ -184,6 +184,42 @@ namespace falcon { return detail::api().entitySetAttribute(mHandle, name.c_str(), value) != 0; } + /** + * Text this plugin keeps on the entity or player, saved with it. Keys belong to the plugin: another plugin + * never sees them. Entities without saved state, such as dropped items, keep nothing. + */ + std::optional data(const std::string &key) const { + const char *value = detail::api().entityData(detail::plugin(), mHandle, key.c_str()); + if (value == nullptr) + return std::nullopt; + return std::string(value); + } + + bool setData(const std::string &key, const std::string &value) const { + return detail::api().entitySetData(detail::plugin(), mHandle, key.c_str(), value.c_str()) != 0; + } + + bool removeData(const std::string &key) const { + return detail::api().entitySetData(detail::plugin(), mHandle, key.c_str(), nullptr) != 0; + } + + std::vector dataKeys() const { + std::vector keys; + const uint32_t count = detail::api().entityDataCount(detail::plugin(), mHandle); + for (uint32_t index = 0; index < count; index++) + keys.push_back(detail::text(detail::api().entityDataKey(detail::plugin(), mHandle, index))); + return keys; + } + + /** What the server would save for this entity, as little-endian NBT; read only. */ + std::vector nbt() const { + uint32_t length = 0; + const uint8_t *data = detail::api().entityNbt(mHandle, &length); + if (data == nullptr) + return {}; + return std::vector(data, data + length); + } + std::optional asPlayer() const; FalconEntity *handle() const { diff --git a/native/include/falcon/Item.hpp b/native/include/falcon/Item.hpp index 7042d91..30b7477 100644 --- a/native/include/falcon/Item.hpp +++ b/native/include/falcon/Item.hpp @@ -109,6 +109,47 @@ namespace falcon { detail::api().itemSetEnchantmentLevel(mHandle, enchantment, level); } + /** + * Text this plugin keeps on the item, saved with the item wherever it goes. Keys belong to the plugin: another + * plugin never sees them. A copy of an inventory item must be put back for the change to stay. + */ + std::optional data(const std::string &key) const { + const char *value = detail::api().itemData(detail::plugin(), mHandle, key.c_str()); + if (value == nullptr) + return std::nullopt; + return std::string(value); + } + + bool setData(const std::string &key, const std::string &value) const { + return detail::api().itemSetData(detail::plugin(), mHandle, key.c_str(), value.c_str()) != 0; + } + + bool removeData(const std::string &key) const { + return detail::api().itemSetData(detail::plugin(), mHandle, key.c_str(), nullptr) != 0; + } + + std::vector dataKeys() const { + std::vector keys; + const uint32_t count = detail::api().itemDataCount(detail::plugin(), mHandle); + for (uint32_t index = 0; index < count; index++) + keys.push_back(detail::text(detail::api().itemDataKey(detail::plugin(), mHandle, index))); + return keys; + } + + /** The item's tag as little-endian NBT, the game's save format; empty for an item without one. */ + std::vector nbt() const { + uint32_t length = 0; + const uint8_t *data = detail::api().itemNbt(mHandle, &length); + if (data == nullptr) + return {}; + return std::vector(data, data + length); + } + + /** Replaces the item's tag; empty data removes it. False when the data is not a little-endian NBT compound. */ + bool setNbt(const std::vector &data) const { + return detail::api().itemSetNbt(mHandle, data.data(), static_cast(data.size())) != 0; + } + FalconItem *handle() const { return mHandle; } diff --git a/native/include/falcon/falcon_api.h b/native/include/falcon/falcon_api.h index 772d653..612611b 100644 --- a/native/include/falcon/falcon_api.h +++ b/native/include/falcon/falcon_api.h @@ -565,6 +565,18 @@ typedef struct FalconServerApi { uint32_t (*playerEnderChestSize)(FalconPlayer *player); FalconItem *(*playerEnderChestItem)(FalconPlayer *player, uint32_t slot); int (*playerSetEnderChestItem)(FalconPlayer *player, uint32_t slot, FalconItem *item); + + const char *(*entityData)(FalconPlugin *plugin, FalconEntity *entity, const char *key); + int (*entitySetData)(FalconPlugin *plugin, FalconEntity *entity, const char *key, const char *value); + uint32_t (*entityDataCount)(FalconPlugin *plugin, FalconEntity *entity); + const char *(*entityDataKey)(FalconPlugin *plugin, FalconEntity *entity, uint32_t index); + const char *(*itemData)(FalconPlugin *plugin, FalconItem *item, const char *key); + int (*itemSetData)(FalconPlugin *plugin, FalconItem *item, const char *key, const char *value); + uint32_t (*itemDataCount)(FalconPlugin *plugin, FalconItem *item); + const char *(*itemDataKey)(FalconPlugin *plugin, FalconItem *item, uint32_t index); + 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); } FalconServerApi; typedef int (*FalconPluginEntry)(const FalconServerApi *api, FalconPlugin *plugin, FalconPluginCallbacks *callbacks); diff --git a/wasm/falcon.wit b/wasm/falcon.wit index 0e4fa88..f4b9ee0 100644 --- a/wasm/falcon.wit +++ b/wasm/falcon.wit @@ -188,6 +188,14 @@ interface entities { entity-attribute-name: func(entity: entity, index: u32) -> option; entity-attribute: func(entity: entity, name: string) -> option; entity-set-attribute: func(entity: entity, name: string, value: f32) -> bool; + /// Text kept on the entity for this plugin only, saved with it. + entity-data: func(entity: entity, key: string) -> option; + /// `none` removes the key. + entity-set-data: func(entity: entity, key: string, value: option) -> bool; + entity-data-count: func(entity: entity) -> u32; + entity-data-key: func(entity: entity, index: u32) -> option; + /// What the server would save for the entity, as little-endian NBT. + entity-nbt: func(entity: entity) -> list; } interface levels { @@ -281,6 +289,16 @@ interface items { 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); + /// Text kept on the item for this plugin only, saved with the item. + item-data: func(item: borrow, key: string) -> option; + /// `none` removes the key. + item-set-data: func(item: borrow, key: string, value: option) -> bool; + item-data-count: func(item: borrow) -> u32; + item-data-key: func(item: borrow, index: u32) -> option; + /// The item's tag as little-endian NBT; empty without one. + item-nbt: func(item: borrow) -> list; + /// An empty list removes the tag. + item-set-nbt: func(item: borrow, data: list) -> bool; 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>);