diff --git a/README.md b/README.md index f629bca..19ce4b9 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,21 @@ body.level().spawnParticle("minecraft:heart_particle", body.position()); A sound or particle sent through a `Player` reaches only that player; through a `Level`, every player in it. +### Scoreboard + +`falcon::Scoreboard` is the server's own scoreboard, the one `/scoreboard` and scripts use, saved with the default +world. A participant is an entity, a player through `player.entity()`, or a fake player named by text: + +```cpp +falcon::Scoreboard::addObjective("kills", "Kills"); +falcon::Scoreboard::setDisplay(falcon::DisplaySlot::Sidebar, "kills"); +falcon::Scoreboard::setScore("kills", player.entity(), 3); +falcon::Scoreboard::setScore("kills", "Herobrine", 99); + +for (const falcon::Score &score: falcon::Scoreboard::scores("kills")) + logger().info(score.name + ": " + std::to_string(score.value)); +``` + ### 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/listeners/JoinListener.java b/examples/java/HelloPlugin/src/main/java/falcon/examples/hello/listeners/JoinListener.java index cbd6adf..ac6b824 100644 --- a/examples/java/HelloPlugin/src/main/java/falcon/examples/hello/listeners/JoinListener.java +++ b/examples/java/HelloPlugin/src/main/java/falcon/examples/hello/listeners/JoinListener.java @@ -1,9 +1,11 @@ package falcon.examples.hello.listeners; +import falcon.api.DisplaySlot; import falcon.api.EffectType; import falcon.api.Entity; import falcon.api.Player; import falcon.api.Plugin; +import falcon.api.Scoreboard; import falcon.api.event.PlayerJoinEvent; import falcon.api.event.PlayerQuitEvent; import falcon.examples.hello.storage.JoinStore; @@ -13,6 +15,10 @@ private JoinListener() { } public static void registerTo(Plugin plugin, JoinStore joins) { + Scoreboard scoreboard = plugin.server().scoreboard(); + scoreboard.addObjective("joins", "Visits"); + scoreboard.setDisplay(DisplaySlot.LIST, "joins"); + plugin.events().on(PlayerJoinEvent.class, event -> { Player player = event.player(); int count = joins.recordJoin(player.name()); @@ -24,6 +30,7 @@ public static void registerTo(Plugin plugin, JoinStore joins) { } Entity body = player.entity(); + scoreboard.setScore("joins", body, count); body.addEffect(EffectType.SPEED, 200, 1, true); player.playSound("random.levelup", body.position()); player.spawnParticle("minecraft:villager_happy", body.position()); diff --git a/examples/native/HelloPlugin/src/Listeners/JoinListener.cpp b/examples/native/HelloPlugin/src/Listeners/JoinListener.cpp index 7250868..05c96ab 100644 --- a/examples/native/HelloPlugin/src/Listeners/JoinListener.cpp +++ b/examples/native/HelloPlugin/src/Listeners/JoinListener.cpp @@ -5,6 +5,9 @@ #include void JoinListener::registerTo(const falcon::Plugin &plugin, JoinStore &joins) { + falcon::Scoreboard::addObjective("joins", "Visits"); + falcon::Scoreboard::setDisplay(falcon::DisplaySlot::List, "joins"); + plugin.events().on([&joins](falcon::PlayerJoinEvent &event) { const falcon::Player player = event.player(); const uint32_t count = joins.recordJoin(player.name()); @@ -15,6 +18,7 @@ void JoinListener::registerTo(const falcon::Plugin &plugin, JoinStore &joins) { player.sendMessage("Welcome back, " + player.name() + "! Visit number " + std::to_string(count) + "."); const falcon::Entity body = player.entity(); + falcon::Scoreboard::setScore("joins", body, (int32_t) count); body.addEffect(falcon::EffectType::Speed, 200, 1); player.playSound("random.levelup", body.position()); player.spawnParticle("minecraft:villager_happy", body.position()); diff --git a/java/src/main/java/falcon/api/DisplaySlot.java b/java/src/main/java/falcon/api/DisplaySlot.java new file mode 100644 index 0000000..8ac349d --- /dev/null +++ b/java/src/main/java/falcon/api/DisplaySlot.java @@ -0,0 +1,19 @@ +package falcon.api; + +import falcon.api.internal.FalconAbi; + +public enum DisplaySlot { + SIDEBAR(FalconAbi.DISPLAY_SLOT_SIDEBAR), + LIST(FalconAbi.DISPLAY_SLOT_LIST), + BELOW_NAME(FalconAbi.DISPLAY_SLOT_BELOW_NAME); + + private final int mValue; + + DisplaySlot(int value) { + mValue = value; + } + + public int value() { + return mValue; + } +} diff --git a/java/src/main/java/falcon/api/Score.java b/java/src/main/java/falcon/api/Score.java new file mode 100644 index 0000000..3dbb832 --- /dev/null +++ b/java/src/main/java/falcon/api/Score.java @@ -0,0 +1,10 @@ +package falcon.api; + +import java.util.Optional; + +/** + * A score of an objective. The name is the player's name, the entity's name tag or the fake player's name, empty + * for an unloaded entity; the entity is present while the player is online or the entity loaded. + */ +public record Score(String name, int value, Optional entity) { +} diff --git a/java/src/main/java/falcon/api/Scoreboard.java b/java/src/main/java/falcon/api/Scoreboard.java new file mode 100644 index 0000000..3ea1416 --- /dev/null +++ b/java/src/main/java/falcon/api/Scoreboard.java @@ -0,0 +1,146 @@ +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.lang.foreign.ValueLayout; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +/** + * The server's scoreboard, shared with commands and scripts and saved with the default world. Objectives are dummy + * objectives. A participant is an entity, a player through its entity, or a fake player named by text. + */ +public final class Scoreboard { + private final PluginContext mContext; + + Scoreboard(PluginContext context) { + mContext = context; + } + + /** Returns false when the name is taken. */ + public boolean addObjective(String name, String displayName) { + try (Arena arena = Arena.ofConfined()) { + return api().scoreboardAddObjective(Interop.text(arena, name), Interop.text(arena, displayName)) != 0; + } + } + + public boolean removeObjective(String name) { + try (Arena arena = Arena.ofConfined()) { + return api().scoreboardRemoveObjective(Interop.text(arena, name)) != 0; + } + } + + public List objectives() { + int count = api().scoreboardObjectiveCount(); + List names = new ArrayList<>(Math.max(count, 0)); + for (int index = 0; index < count; index++) { + names.add(Interop.string(api().scoreboardObjectiveName(index))); + } + return names; + } + + public Optional displayName(String objective) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment name = api().scoreboardObjectiveDisplayName(Interop.text(arena, objective)); + return Interop.isNull(name) ? Optional.empty() : Optional.of(Interop.string(name)); + } + } + + public OptionalInt score(String objective, Entity entity) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment value = arena.allocate(ValueLayout.JAVA_INT); + if (api().scoreboardEntityScore(Interop.text(arena, objective), entity.handle(), value) == 0) { + return OptionalInt.empty(); + } + return OptionalInt.of(value.get(ValueLayout.JAVA_INT, 0)); + } + } + + public boolean setScore(String objective, Entity entity, int value) { + try (Arena arena = Arena.ofConfined()) { + return api().scoreboardSetEntityScore(Interop.text(arena, objective), entity.handle(), value) != 0; + } + } + + public boolean resetScore(String objective, Entity entity) { + try (Arena arena = Arena.ofConfined()) { + return api().scoreboardResetEntityScore(Interop.text(arena, objective), entity.handle()) != 0; + } + } + + public OptionalInt score(String objective, String fakePlayer) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment value = arena.allocate(ValueLayout.JAVA_INT); + if (api().scoreboardFakePlayerScore(Interop.text(arena, objective), Interop.text(arena, fakePlayer), + value) == 0) { + return OptionalInt.empty(); + } + return OptionalInt.of(value.get(ValueLayout.JAVA_INT, 0)); + } + } + + public boolean setScore(String objective, String fakePlayer, int value) { + try (Arena arena = Arena.ofConfined()) { + return api().scoreboardSetFakePlayerScore(Interop.text(arena, objective), Interop.text(arena, fakePlayer), + value) != 0; + } + } + + public boolean resetScore(String objective, String fakePlayer) { + try (Arena arena = Arena.ofConfined()) { + return api().scoreboardResetFakePlayerScore(Interop.text(arena, objective), + Interop.text(arena, fakePlayer)) != 0; + } + } + + /** Every score of an objective, in the order the participants joined it. */ + public List scores(String objective) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment name = Interop.text(arena, objective); + MemorySegment value = arena.allocate(ValueLayout.JAVA_INT); + int count = api().scoreboardScoreCount(name); + List result = new ArrayList<>(Math.max(count, 0)); + for (int index = 0; index < count; index++) { + MemorySegment participant = api().scoreboardScoreAt(name, index, value); + if (Interop.isNull(participant)) { + continue; + } + MemorySegment holder = api().scoreboardScoreEntity(name, index); + Optional entity = Interop.isNull(holder) ? Optional.empty() + : Optional.of(new Entity(mContext, holder)); + result.add(new Score(Interop.string(participant), value.get(ValueLayout.JAVA_INT, 0), entity)); + } + return result; + } + } + + public boolean setDisplay(DisplaySlot slot, String objective) { + return setDisplay(slot, objective, false); + } + + /** Shows an objective in a slot, highest score first unless ascending. */ + public boolean setDisplay(DisplaySlot slot, String objective, boolean ascending) { + try (Arena arena = Arena.ofConfined()) { + return api().scoreboardSetDisplay(slot.value(), Interop.text(arena, objective), ascending ? 1 : 0) != 0; + } + } + + public void clearDisplay(DisplaySlot slot) { + api().scoreboardSetDisplay(slot.value(), MemorySegment.NULL, 0); + } + + public Optional displayed(DisplaySlot slot) { + MemorySegment name = api().scoreboardDisplayObjective(slot.value()); + return Interop.isNull(name) ? Optional.empty() : Optional.of(Interop.string(name)); + } + + 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 11a4285..689d30c 100644 --- a/java/src/main/java/falcon/api/Server.java +++ b/java/src/main/java/falcon/api/Server.java @@ -97,6 +97,10 @@ public boolean unloadWorld(String name) { } } + public Scoreboard scoreboard() { + return new Scoreboard(mContext); + } + private static NativeApi api() { return Interop.api(); } diff --git a/native/include/falcon/Falcon.hpp b/native/include/falcon/Falcon.hpp index 79189f2..bcd9d58 100644 --- a/native/include/falcon/Falcon.hpp +++ b/native/include/falcon/Falcon.hpp @@ -25,6 +25,7 @@ #include "falcon/PlayerInventory.hpp" #include "falcon/Plugin.hpp" #include "falcon/Scheduler.hpp" +#include "falcon/Scoreboard.hpp" #include "falcon/Server.hpp" #include "falcon/ServerEvents.hpp" #include "falcon/Services.hpp" diff --git a/native/include/falcon/Scoreboard.hpp b/native/include/falcon/Scoreboard.hpp new file mode 100644 index 0000000..de89a14 --- /dev/null +++ b/native/include/falcon/Scoreboard.hpp @@ -0,0 +1,118 @@ +#pragma once + +#include "falcon/Api.hpp" +#include "falcon/Entity.hpp" + +#include +#include +#include +#include + +namespace falcon { + enum class DisplaySlot : uint32_t { + Sidebar = FALCON_DISPLAY_SLOT_SIDEBAR, + List = FALCON_DISPLAY_SLOT_LIST, + BelowName = FALCON_DISPLAY_SLOT_BELOW_NAME + }; + + struct Score { + /** The player's name, the entity's name tag or the fake player's name; empty for an unloaded entity. */ + std::string name; + int32_t value = 0; + /** The online player or loaded entity holding the score; not valid for a fake player. */ + Entity entity{nullptr}; + }; + + /** + * The server's scoreboard, shared with commands and scripts and saved with the default world. Objectives are + * dummy objectives. A participant is an entity, a player through its entity, or a fake player named by text. + */ + class Scoreboard { + public: + /** False when the name is taken. */ + static bool addObjective(const std::string &name, const std::string &displayName = "") { + return detail::api().scoreboardAddObjective(name.c_str(), displayName.c_str()) != 0; + } + + static bool removeObjective(const std::string &name) { + return detail::api().scoreboardRemoveObjective(name.c_str()) != 0; + } + + static std::vector objectives() { + std::vector names; + const uint32_t count = detail::api().scoreboardObjectiveCount(); + for (uint32_t index = 0; index < count; ++index) + names.push_back(detail::text(detail::api().scoreboardObjectiveName(index))); + return names; + } + + static std::optional displayName(const std::string &objective) { + const char *name = detail::api().scoreboardObjectiveDisplayName(objective.c_str()); + if (name == nullptr) + return std::nullopt; + return std::string(name); + } + + static std::optional score(const std::string &objective, const Entity &entity) { + int32_t value = 0; + if (detail::api().scoreboardEntityScore(objective.c_str(), entity.handle(), &value) == 0) + return std::nullopt; + return value; + } + + static bool setScore(const std::string &objective, const Entity &entity, int32_t value) { + return detail::api().scoreboardSetEntityScore(objective.c_str(), entity.handle(), value) != 0; + } + + static bool resetScore(const std::string &objective, const Entity &entity) { + return detail::api().scoreboardResetEntityScore(objective.c_str(), entity.handle()) != 0; + } + + static std::optional score(const std::string &objective, const std::string &fakePlayer) { + int32_t value = 0; + if (detail::api().scoreboardFakePlayerScore(objective.c_str(), fakePlayer.c_str(), &value) == 0) + return std::nullopt; + return value; + } + + static bool setScore(const std::string &objective, const std::string &fakePlayer, int32_t value) { + return detail::api().scoreboardSetFakePlayerScore(objective.c_str(), fakePlayer.c_str(), value) != 0; + } + + static bool resetScore(const std::string &objective, const std::string &fakePlayer) { + return detail::api().scoreboardResetFakePlayerScore(objective.c_str(), fakePlayer.c_str()) != 0; + } + + /** Every score of an objective, in the order the participants joined it. */ + static std::vector scores(const std::string &objective) { + std::vector result; + const uint32_t count = detail::api().scoreboardScoreCount(objective.c_str()); + for (uint32_t index = 0; index < count; ++index) { + int32_t value = 0; + const char *name = detail::api().scoreboardScoreAt(objective.c_str(), index, &value); + if (name == nullptr) + continue; + FalconEntity *holder = detail::api().scoreboardScoreEntity(objective.c_str(), index); + result.push_back(Score{name, value, Entity(holder)}); + } + return result; + } + + /** Shows an objective in a slot, highest score first unless ascending. */ + static bool setDisplay(DisplaySlot slot, const std::string &objective, bool ascending = false) { + return detail::api().scoreboardSetDisplay(static_cast(slot), objective.c_str(), + ascending ? 1 : 0) != 0; + } + + static void clearDisplay(DisplaySlot slot) { + detail::api().scoreboardSetDisplay(static_cast(slot), nullptr, 0); + } + + static std::optional displayed(DisplaySlot slot) { + const char *name = detail::api().scoreboardDisplayObjective(static_cast(slot)); + if (name == nullptr) + return std::nullopt; + return std::string(name); + } + }; +} diff --git a/native/include/falcon/falcon_api.h b/native/include/falcon/falcon_api.h index b424b64..69230e7 100644 --- a/native/include/falcon/falcon_api.h +++ b/native/include/falcon/falcon_api.h @@ -183,6 +183,11 @@ typedef uint32_t FalconEffectType; #define FALCON_EFFECT_INFINITE -1 +typedef uint32_t FalconDisplaySlot; +#define FALCON_DISPLAY_SLOT_SIDEBAR 0u +#define FALCON_DISPLAY_SLOT_LIST 1u +#define FALCON_DISPLAY_SLOT_BELOW_NAME 2u + typedef uint32_t FalconLogLevel; #define FALCON_LOG_INFO 0u #define FALCON_LOG_WARNING 1u @@ -530,6 +535,23 @@ typedef struct FalconServerApi { void (*playerStopSound)(FalconPlayer *player, const char *sound); void (*playerSpawnParticle)(FalconPlayer *player, const char *particle, FalconVec3 position); void (*levelSpawnParticle)(FalconLevel *level, const char *particle, FalconVec3 position); + + int (*scoreboardAddObjective)(const char *name, const char *displayName); + int (*scoreboardRemoveObjective)(const char *name); + uint32_t (*scoreboardObjectiveCount)(void); + const char *(*scoreboardObjectiveName)(uint32_t index); + const char *(*scoreboardObjectiveDisplayName)(const char *objective); + int (*scoreboardEntityScore)(const char *objective, FalconEntity *entity, int32_t *score); + int (*scoreboardSetEntityScore)(const char *objective, FalconEntity *entity, int32_t score); + int (*scoreboardResetEntityScore)(const char *objective, FalconEntity *entity); + int (*scoreboardFakePlayerScore)(const char *objective, const char *name, int32_t *score); + int (*scoreboardSetFakePlayerScore)(const char *objective, const char *name, int32_t score); + int (*scoreboardResetFakePlayerScore)(const char *objective, const char *name); + uint32_t (*scoreboardScoreCount)(const char *objective); + const char *(*scoreboardScoreAt)(const char *objective, uint32_t index, int32_t *score); + FalconEntity *(*scoreboardScoreEntity)(const char *objective, uint32_t index); + int (*scoreboardSetDisplay)(FalconDisplaySlot slot, const char *objective, int ascending); + const char *(*scoreboardDisplayObjective)(FalconDisplaySlot slot); } FalconServerApi; typedef int (*FalconPluginEntry)(const FalconServerApi *api, FalconPlugin *plugin, FalconPluginCallbacks *callbacks); diff --git a/wasm/falcon.wit b/wasm/falcon.wit index c58f38d..f0c3258 100644 --- a/wasm/falcon.wit +++ b/wasm/falcon.wit @@ -223,6 +223,37 @@ interface levels { level-spawn-particle: func(level: borrow, particle: string, position: vec3); } +/// The server's scoreboard. A participant is an entity, a player through its entity id, or a fake player by name. +interface scoreboard { + use types.{entity}; + + /// One of the `FALCON_DISPLAY_SLOT_*` values. + type display-slot = u32; + + record score-entry { + name: string, + value: s32, + } + + scoreboard-add-objective: func(name: string, display-name: string) -> bool; + scoreboard-remove-objective: func(name: string) -> bool; + scoreboard-objective-count: func() -> u32; + scoreboard-objective-name: func(index: u32) -> option; + scoreboard-objective-display-name: func(objective: string) -> option; + scoreboard-entity-score: func(objective: string, entity: entity) -> option; + scoreboard-set-entity-score: func(objective: string, entity: entity, score: s32) -> bool; + scoreboard-reset-entity-score: func(objective: string, entity: entity) -> bool; + scoreboard-fake-player-score: func(objective: string, name: string) -> option; + scoreboard-set-fake-player-score: func(objective: string, name: string, score: s32) -> bool; + scoreboard-reset-fake-player-score: func(objective: string, name: string) -> bool; + scoreboard-score-count: func(objective: string) -> u32; + scoreboard-score-at: func(objective: string, index: u32) -> option; + scoreboard-score-entity: func(objective: string, index: u32) -> option; + /// `none` clears the slot. + scoreboard-set-display: func(slot: display-slot, objective: option, ascending: bool) -> bool; + scoreboard-display-objective: func(slot: display-slot) -> option; +} + interface items { use types.{player, item, armor-slot}; @@ -448,6 +479,7 @@ world falcon-plugin { import entities; import levels; import items; + import scoreboard; import events; import event; import commands;