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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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());
Expand All @@ -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());
Expand Down
4 changes: 4 additions & 0 deletions examples/native/HelloPlugin/src/Listeners/JoinListener.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#include <string>

void JoinListener::registerTo(const falcon::Plugin &plugin, JoinStore &joins) {
falcon::Scoreboard::addObjective("joins", "Visits");
falcon::Scoreboard::setDisplay(falcon::DisplaySlot::List, "joins");

plugin.events().on<falcon::PlayerJoinEvent>([&joins](falcon::PlayerJoinEvent &event) {
const falcon::Player player = event.player();
const uint32_t count = joins.recordJoin(player.name());
Expand All @@ -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());
Expand Down
19 changes: 19 additions & 0 deletions java/src/main/java/falcon/api/DisplaySlot.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
10 changes: 10 additions & 0 deletions java/src/main/java/falcon/api/Score.java
Original file line number Diff line number Diff line change
@@ -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> entity) {
}
146 changes: 146 additions & 0 deletions java/src/main/java/falcon/api/Scoreboard.java
Original file line number Diff line number Diff line change
@@ -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<String> objectives() {
int count = api().scoreboardObjectiveCount();
List<String> 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<String> 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<Score> 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<Score> 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> 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<String> 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();
}
}
4 changes: 4 additions & 0 deletions java/src/main/java/falcon/api/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ public boolean unloadWorld(String name) {
}
}

public Scoreboard scoreboard() {
return new Scoreboard(mContext);
}

private static NativeApi api() {
return Interop.api();
}
Expand Down
1 change: 1 addition & 0 deletions native/include/falcon/Falcon.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
118 changes: 118 additions & 0 deletions native/include/falcon/Scoreboard.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#pragma once

#include "falcon/Api.hpp"
#include "falcon/Entity.hpp"

#include <cstdint>
#include <optional>
#include <string>
#include <vector>

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<std::string> objectives() {
std::vector<std::string> 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<std::string> 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<int32_t> 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<int32_t> 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<Score> scores(const std::string &objective) {
std::vector<Score> 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<FalconDisplaySlot>(slot), objective.c_str(),
ascending ? 1 : 0) != 0;
}

static void clearDisplay(DisplaySlot slot) {
detail::api().scoreboardSetDisplay(static_cast<FalconDisplaySlot>(slot), nullptr, 0);
}

static std::optional<std::string> displayed(DisplaySlot slot) {
const char *name = detail::api().scoreboardDisplayObjective(static_cast<FalconDisplaySlot>(slot));
if (name == nullptr)
return std::nullopt;
return std::string(name);
}
};
}
Loading
Loading