Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<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, 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.
Expand Down
16 changes: 16 additions & 0 deletions java/src/main/java/falcon/api/Entity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Player> asPlayer() {
MemorySegment player = api().entityPlayer(mHandle);
if (Interop.isNull(player)) {
Expand Down
27 changes: 27 additions & 0 deletions java/src/main/java/falcon/api/Item.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item> copy() {
if (Interop.isNull(mHandle)) {
return Optional.empty();
Expand Down
90 changes: 90 additions & 0 deletions java/src/main/java/falcon/api/PersistentData.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> keys(Entity entity) {
int count = api().entityDataCount(mContext.handle(), entity.handle());
List<String> 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<String> 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<String> keys(Item item) {
int count = api().itemDataCount(mContext.handle(), item.handle());
List<String> 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<String> optional(MemorySegment value) {
return Interop.isNull(value) ? Optional.empty() : Optional.of(Interop.string(value));
}

private static NativeApi api() {
return Interop.api();
}
}
6 changes: 6 additions & 0 deletions java/src/main/java/falcon/api/Plugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down Expand Up @@ -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"));
Expand Down
36 changes: 36 additions & 0 deletions native/include/falcon/Entity.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> 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<std::string> dataKeys() const {
std::vector<std::string> 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<uint8_t> nbt() const {
uint32_t length = 0;
const uint8_t *data = detail::api().entityNbt(mHandle, &length);
if (data == nullptr)
return {};
return std::vector<uint8_t>(data, data + length);
}

std::optional<Player> asPlayer() const;

FalconEntity *handle() const {
Expand Down
41 changes: 41 additions & 0 deletions native/include/falcon/Item.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> 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<std::string> dataKeys() const {
std::vector<std::string> 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<uint8_t> nbt() const {
uint32_t length = 0;
const uint8_t *data = detail::api().itemNbt(mHandle, &length);
if (data == nullptr)
return {};
return std::vector<uint8_t>(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<uint8_t> &data) const {
return detail::api().itemSetNbt(mHandle, data.data(), static_cast<uint32_t>(data.size())) != 0;
}

FalconItem *handle() const {
return mHandle;
}
Expand Down
12 changes: 12 additions & 0 deletions native/include/falcon/falcon_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions wasm/falcon.wit
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ interface entities {
entity-attribute-name: func(entity: entity, index: u32) -> option<string>;
entity-attribute: func(entity: entity, name: string) -> option<attribute>;
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<string>;
/// `none` removes the key.
entity-set-data: func(entity: entity, key: string, value: option<string>) -> bool;
entity-data-count: func(entity: entity) -> u32;
entity-data-key: func(entity: entity, index: u32) -> option<string>;
/// What the server would save for the entity, as little-endian NBT.
entity-nbt: func(entity: entity) -> list<u8>;
}

interface levels {
Expand Down Expand Up @@ -281,6 +289,16 @@ interface items {
item-set-lore: func(item: borrow<item>, lines: list<string>);
item-enchantment-level: func(item: borrow<item>, enchantment: u32) -> s32;
item-set-enchantment-level: func(item: borrow<item>, enchantment: u32, level: s32);
/// Text kept on the item for this plugin only, saved with the item.
item-data: func(item: borrow<item>, key: string) -> option<string>;
/// `none` removes the key.
item-set-data: func(item: borrow<item>, key: string, value: option<string>) -> bool;
item-data-count: func(item: borrow<item>) -> u32;
item-data-key: func(item: borrow<item>, index: u32) -> option<string>;
/// The item's tag as little-endian NBT; empty without one.
item-nbt: func(item: borrow<item>) -> list<u8>;
/// An empty list removes the tag.
item-set-nbt: func(item: borrow<item>, data: list<u8>) -> bool;
player-inventory-size: func(player: player) -> u32;
player-inventory-item: func(player: player, slot: u32) -> option<item>;
player-set-inventory-item: func(player: player, slot: u32, item: option<borrow<item>>);
Expand Down
Loading