diff --git a/README.md b/README.md
index 06a0e8e..f629bca 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
@@ -123,6 +123,26 @@ falcon::CustomEventResult result = services().fire("quests:completed", "{\"quest
A service belongs to the plugin that provided it and disappears when that plugin is disabled. Call services and
fire custom events from the main thread.
+### Effects, attributes, sounds and particles
+
+Potion effects and attributes work on players and entities alike, through `Entity`. A duration is in ticks,
+`falcon::Effect::INFINITE` for an endless effect, and an effect weaker or shorter than the running one is refused
+as in the game. Attributes are the ones the entity has, by identifier, such as `minecraft:movement`; a value
+outside the attribute's range is refused.
+
+```cpp
+const falcon::Entity body = player.entity();
+body.addEffect(falcon::EffectType::Speed, 200, 1);
+std::optional speed = body.attribute("minecraft:movement");
+body.setAttribute("minecraft:movement", 0.15f);
+
+player.playSound("random.levelup", body.position());
+body.level().playSound("mob.wolf.bark", body.position(), 1.0f, 1.2f);
+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.
+
### C# plugins
The `dotnet` runtime is deprecated. Write new plugins for Wasm, native C++ or Java.
@@ -137,7 +157,7 @@ A C# plugin is a `net8.0` class library that references `Falcon.PluginAPI` and s
-
+
```
@@ -193,7 +213,7 @@ A Java plugin is a Java 22 jar that depends on `falcon-plugin-api` with the `pro
io.github.falcon-mc
falcon-plugin-api
- 1.0.5
+ 1.0.6
provided
```
@@ -222,7 +242,7 @@ full class name and `jar` defaults to `.jar`:
{
"name": "MyPlugin",
"version": "1.0.0",
- "api-version": "1.0.5",
+ "api-version": "1.0.6",
"main": "myplugin.MyPlugin",
"runtime": "java",
"jar": "MyPlugin.jar"
diff --git a/examples/java/HelloPlugin/plugin.json b/examples/java/HelloPlugin/plugin.json
index 53e03be..2210051 100644
--- a/examples/java/HelloPlugin/plugin.json
+++ b/examples/java/HelloPlugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "HelloPlugin",
"version": "1.0.0",
- "api-version": "1.0.5",
+ "api-version": "1.0.6",
"main": "falcon.examples.hello.HelloPlugin",
"runtime": "java",
"jar": "HelloPlugin.jar",
diff --git a/examples/java/HelloPlugin/pom.xml b/examples/java/HelloPlugin/pom.xml
index 3af2367..271c122 100644
--- a/examples/java/HelloPlugin/pom.xml
+++ b/examples/java/HelloPlugin/pom.xml
@@ -15,7 +15,7 @@
22
UTF-8
- 1.0.5
+ 1.0.6
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 958af09..cbd6adf 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,5 +1,7 @@
package falcon.examples.hello.listeners;
+import falcon.api.EffectType;
+import falcon.api.Entity;
import falcon.api.Player;
import falcon.api.Plugin;
import falcon.api.event.PlayerJoinEvent;
@@ -20,6 +22,11 @@ public static void registerTo(Plugin plugin, JoinStore joins) {
} else {
player.sendMessage("Welcome back, " + player.name() + "! Visit number " + count + ".");
}
+
+ Entity body = player.entity();
+ body.addEffect(EffectType.SPEED, 200, 1, true);
+ player.playSound("random.levelup", body.position());
+ player.spawnParticle("minecraft:villager_happy", body.position());
});
plugin.events().on(PlayerQuitEvent.class, event -> {
diff --git a/examples/native/HelloPlugin/src/Listeners/JoinListener.cpp b/examples/native/HelloPlugin/src/Listeners/JoinListener.cpp
index 0de5b43..7250868 100644
--- a/examples/native/HelloPlugin/src/Listeners/JoinListener.cpp
+++ b/examples/native/HelloPlugin/src/Listeners/JoinListener.cpp
@@ -13,6 +13,11 @@ void JoinListener::registerTo(const falcon::Plugin &plugin, JoinStore &joins) {
player.sendMessage("Welcome to the server, " + player.name() + "!");
else
player.sendMessage("Welcome back, " + player.name() + "! Visit number " + std::to_string(count) + ".");
+
+ const falcon::Entity body = player.entity();
+ body.addEffect(falcon::EffectType::Speed, 200, 1);
+ player.playSound("random.levelup", body.position());
+ player.spawnParticle("minecraft:villager_happy", body.position());
});
plugin.events().on([&plugin](falcon::PlayerQuitEvent &event) {
diff --git a/java/pom.xml b/java/pom.xml
index 7654ea9..18dc25f 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -6,7 +6,7 @@
io.github.falcon-mc
falcon-plugin-api
- 1.0.5
+ 1.0.6
jar
Falcon Plugin API
diff --git a/java/src/main/java/falcon/api/Attribute.java b/java/src/main/java/falcon/api/Attribute.java
new file mode 100644
index 0000000..e3ac47b
--- /dev/null
+++ b/java/src/main/java/falcon/api/Attribute.java
@@ -0,0 +1,15 @@
+package falcon.api;
+
+import falcon.api.internal.FalconAbi;
+
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.ValueLayout;
+
+public record Attribute(String name, float value, float minimum, float maximum, float defaultValue) {
+ static Attribute read(String name, MemorySegment segment) {
+ return new Attribute(name, segment.get(ValueLayout.JAVA_FLOAT, FalconAbi.Attribute.VALUE),
+ segment.get(ValueLayout.JAVA_FLOAT, FalconAbi.Attribute.MINIMUM),
+ segment.get(ValueLayout.JAVA_FLOAT, FalconAbi.Attribute.MAXIMUM),
+ segment.get(ValueLayout.JAVA_FLOAT, FalconAbi.Attribute.DEFAULT_VALUE));
+ }
+}
diff --git a/java/src/main/java/falcon/api/Effect.java b/java/src/main/java/falcon/api/Effect.java
new file mode 100644
index 0000000..b0f0739
--- /dev/null
+++ b/java/src/main/java/falcon/api/Effect.java
@@ -0,0 +1,21 @@
+package falcon.api;
+
+import falcon.api.internal.FalconAbi;
+
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.ValueLayout;
+
+/**
+ * A potion effect on an entity. The duration is in ticks, {@link #INFINITE} for an endless effect.
+ */
+public record Effect(EffectType type, int duration, int amplifier, boolean showParticles, boolean ambient) {
+ public static final int INFINITE = FalconAbi.EFFECT_INFINITE;
+
+ static Effect read(MemorySegment segment) {
+ return new Effect(EffectType.of(segment.get(ValueLayout.JAVA_INT, FalconAbi.Effect.TYPE)),
+ segment.get(ValueLayout.JAVA_INT, FalconAbi.Effect.DURATION),
+ segment.get(ValueLayout.JAVA_INT, FalconAbi.Effect.AMPLIFIER),
+ segment.get(ValueLayout.JAVA_INT, FalconAbi.Effect.SHOW_PARTICLES) != 0,
+ segment.get(ValueLayout.JAVA_INT, FalconAbi.Effect.AMBIENT) != 0);
+ }
+}
diff --git a/java/src/main/java/falcon/api/EffectType.java b/java/src/main/java/falcon/api/EffectType.java
new file mode 100644
index 0000000..862657e
--- /dev/null
+++ b/java/src/main/java/falcon/api/EffectType.java
@@ -0,0 +1,58 @@
+package falcon.api;
+
+import falcon.api.internal.FalconAbi;
+
+public enum EffectType {
+ SPEED(FalconAbi.EFFECT_SPEED),
+ SLOWNESS(FalconAbi.EFFECT_SLOWNESS),
+ HASTE(FalconAbi.EFFECT_HASTE),
+ MINING_FATIGUE(FalconAbi.EFFECT_MINING_FATIGUE),
+ STRENGTH(FalconAbi.EFFECT_STRENGTH),
+ INSTANT_HEALTH(FalconAbi.EFFECT_INSTANT_HEALTH),
+ INSTANT_DAMAGE(FalconAbi.EFFECT_INSTANT_DAMAGE),
+ JUMP_BOOST(FalconAbi.EFFECT_JUMP_BOOST),
+ NAUSEA(FalconAbi.EFFECT_NAUSEA),
+ REGENERATION(FalconAbi.EFFECT_REGENERATION),
+ RESISTANCE(FalconAbi.EFFECT_RESISTANCE),
+ FIRE_RESISTANCE(FalconAbi.EFFECT_FIRE_RESISTANCE),
+ WATER_BREATHING(FalconAbi.EFFECT_WATER_BREATHING),
+ INVISIBILITY(FalconAbi.EFFECT_INVISIBILITY),
+ BLINDNESS(FalconAbi.EFFECT_BLINDNESS),
+ NIGHT_VISION(FalconAbi.EFFECT_NIGHT_VISION),
+ HUNGER(FalconAbi.EFFECT_HUNGER),
+ WEAKNESS(FalconAbi.EFFECT_WEAKNESS),
+ POISON(FalconAbi.EFFECT_POISON),
+ WITHER(FalconAbi.EFFECT_WITHER),
+ HEALTH_BOOST(FalconAbi.EFFECT_HEALTH_BOOST),
+ ABSORPTION(FalconAbi.EFFECT_ABSORPTION),
+ SATURATION(FalconAbi.EFFECT_SATURATION),
+ LEVITATION(FalconAbi.EFFECT_LEVITATION),
+ FATAL_POISON(FalconAbi.EFFECT_FATAL_POISON),
+ CONDUIT_POWER(FalconAbi.EFFECT_CONDUIT_POWER),
+ SLOW_FALLING(FalconAbi.EFFECT_SLOW_FALLING),
+ BAD_OMEN(FalconAbi.EFFECT_BAD_OMEN),
+ VILLAGE_HERO(FalconAbi.EFFECT_VILLAGE_HERO),
+ DARKNESS(FalconAbi.EFFECT_DARKNESS),
+ WEAVING(FalconAbi.EFFECT_WEAVING),
+ RAID_OMEN(FalconAbi.EFFECT_RAID_OMEN),
+ FREEZING(FalconAbi.EFFECT_FREEZING);
+
+ private final int mValue;
+
+ EffectType(int value) {
+ mValue = value;
+ }
+
+ public int value() {
+ return mValue;
+ }
+
+ public static EffectType of(int value) {
+ for (EffectType type : values()) {
+ if (type.mValue == value) {
+ return type;
+ }
+ }
+ throw new IllegalArgumentException("Unknown effect " + value);
+ }
+}
diff --git a/java/src/main/java/falcon/api/Entity.java b/java/src/main/java/falcon/api/Entity.java
index 0f64a2a..8b04dcb 100644
--- a/java/src/main/java/falcon/api/Entity.java
+++ b/java/src/main/java/falcon/api/Entity.java
@@ -1,11 +1,14 @@
package falcon.api;
+import falcon.api.internal.FalconAbi;
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;
public final class Entity {
@@ -142,6 +145,86 @@ public void extinguish() {
setOnFire(0);
}
+ public boolean addEffect(EffectType type, int duration) {
+ return addEffect(type, duration, 0, true);
+ }
+
+ /**
+ * Adds a potion effect for duration ticks, or {@link Effect#INFINITE}. A weaker or shorter effect than the one
+ * running is refused, as in the game.
+ */
+ public boolean addEffect(EffectType type, int duration, int amplifier, boolean showParticles) {
+ return api().entityAddEffect(mHandle, type.value(), duration, amplifier, showParticles ? 1 : 0) != 0;
+ }
+
+ public boolean removeEffect(EffectType type) {
+ return api().entityRemoveEffect(mHandle, type.value()) != 0;
+ }
+
+ public Optional effect(EffectType type) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment out = arena.allocate(FalconAbi.Effect.LAYOUT);
+ if (api().entityEffect(mHandle, type.value(), out) == 0) {
+ return Optional.empty();
+ }
+ return Optional.of(Effect.read(out));
+ }
+ }
+
+ public boolean hasEffect(EffectType type) {
+ return effect(type).isPresent();
+ }
+
+ public List effects() {
+ int count = api().entityEffectCount(mHandle);
+ List result = new ArrayList<>(Math.max(count, 0));
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment out = arena.allocate(FalconAbi.Effect.LAYOUT);
+ for (int index = 0; index < count; index++) {
+ if (api().entityEffectAt(mHandle, index, out) != 0) {
+ result.add(Effect.read(out));
+ }
+ }
+ }
+ return result;
+ }
+
+ public void clearEffects() {
+ api().entityClearEffects(mHandle);
+ }
+
+ /**
+ * An attribute by its identifier, such as minecraft:movement or minecraft:health. Only the attributes the entity
+ * has are found: {@link #attributes()} lists them.
+ */
+ public Optional attribute(String name) {
+ try (Arena arena = Arena.ofConfined()) {
+ MemorySegment out = arena.allocate(FalconAbi.Attribute.LAYOUT);
+ if (api().entityAttribute(mHandle, Interop.text(arena, name), out) == 0) {
+ return Optional.empty();
+ }
+ return Optional.of(Attribute.read(name, out));
+ }
+ }
+
+ public List attributes() {
+ int count = api().entityAttributeCount(mHandle);
+ List result = new ArrayList<>(Math.max(count, 0));
+ for (int index = 0; index < count; index++) {
+ attribute(Interop.string(api().entityAttributeName(mHandle, index))).ifPresent(result::add);
+ }
+ return result;
+ }
+
+ /**
+ * Returns false when the entity has no such attribute or the value is outside its range.
+ */
+ public boolean setAttribute(String name, float value) {
+ try (Arena arena = Arena.ofConfined()) {
+ return api().entitySetAttribute(mHandle, Interop.text(arena, name), value) != 0;
+ }
+ }
+
public Optional asPlayer() {
MemorySegment player = api().entityPlayer(mHandle);
if (Interop.isNull(player)) {
diff --git a/java/src/main/java/falcon/api/Level.java b/java/src/main/java/falcon/api/Level.java
index f9df143..3e713fe 100644
--- a/java/src/main/java/falcon/api/Level.java
+++ b/java/src/main/java/falcon/api/Level.java
@@ -140,6 +140,25 @@ public void createExplosion(Vec3 position, float power, boolean breakBlocks) {
}
}
+ public void playSound(String sound, Vec3 position) {
+ playSound(sound, position, 1.0f, 1.0f);
+ }
+
+ /**
+ * Plays a sound for every player in this level; the client fades it with distance.
+ */
+ public void playSound(String sound, Vec3 position, float volume, float pitch) {
+ try (Arena arena = Arena.ofConfined()) {
+ api().levelPlaySound(mHandle, Interop.text(arena, sound), Interop.vec3(arena, position), volume, pitch);
+ }
+ }
+
+ public void spawnParticle(String particle, Vec3 position) {
+ try (Arena arena = Arena.ofConfined()) {
+ api().levelSpawnParticle(mHandle, Interop.text(arena, particle), Interop.vec3(arena, position));
+ }
+ }
+
public List entities() {
int count = api().levelEntityCount(mHandle);
List result = new ArrayList<>(count);
diff --git a/java/src/main/java/falcon/api/Player.java b/java/src/main/java/falcon/api/Player.java
index 4e236ae..c3318c4 100644
--- a/java/src/main/java/falcon/api/Player.java
+++ b/java/src/main/java/falcon/api/Player.java
@@ -142,6 +142,38 @@ public void sendActionBar(String message) {
}
}
+ public void playSound(String sound, Vec3 position) {
+ playSound(sound, position, 1.0f, 1.0f);
+ }
+
+ /**
+ * Plays a sound only this player hears, from a position in its level.
+ */
+ public void playSound(String sound, Vec3 position, float volume, float pitch) {
+ try (Arena arena = Arena.ofConfined()) {
+ api().playerPlaySound(mHandle, Interop.text(arena, sound), Interop.vec3(arena, position), volume, pitch);
+ }
+ }
+
+ public void stopAllSounds() {
+ api().playerStopSound(mHandle, MemorySegment.NULL);
+ }
+
+ public void stopSound(String sound) {
+ try (Arena arena = Arena.ofConfined()) {
+ api().playerStopSound(mHandle, Interop.nullableText(arena, sound));
+ }
+ }
+
+ /**
+ * Shows a particle effect only this player sees, in its level.
+ */
+ public void spawnParticle(String particle, Vec3 position) {
+ try (Arena arena = Arena.ofConfined()) {
+ api().playerSpawnParticle(mHandle, Interop.text(arena, particle), Interop.vec3(arena, position));
+ }
+ }
+
public float food() {
return api().playerFood(mHandle);
}
diff --git a/native/include/falcon/Effects.hpp b/native/include/falcon/Effects.hpp
new file mode 100644
index 0000000..3ef22e9
--- /dev/null
+++ b/native/include/falcon/Effects.hpp
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "falcon/falcon_api.h"
+
+#include
+#include
+
+namespace falcon {
+ enum class EffectType : uint32_t {
+ Speed = FALCON_EFFECT_SPEED,
+ Slowness = FALCON_EFFECT_SLOWNESS,
+ Haste = FALCON_EFFECT_HASTE,
+ MiningFatigue = FALCON_EFFECT_MINING_FATIGUE,
+ Strength = FALCON_EFFECT_STRENGTH,
+ InstantHealth = FALCON_EFFECT_INSTANT_HEALTH,
+ InstantDamage = FALCON_EFFECT_INSTANT_DAMAGE,
+ JumpBoost = FALCON_EFFECT_JUMP_BOOST,
+ Nausea = FALCON_EFFECT_NAUSEA,
+ Regeneration = FALCON_EFFECT_REGENERATION,
+ Resistance = FALCON_EFFECT_RESISTANCE,
+ FireResistance = FALCON_EFFECT_FIRE_RESISTANCE,
+ WaterBreathing = FALCON_EFFECT_WATER_BREATHING,
+ Invisibility = FALCON_EFFECT_INVISIBILITY,
+ Blindness = FALCON_EFFECT_BLINDNESS,
+ NightVision = FALCON_EFFECT_NIGHT_VISION,
+ Hunger = FALCON_EFFECT_HUNGER,
+ Weakness = FALCON_EFFECT_WEAKNESS,
+ Poison = FALCON_EFFECT_POISON,
+ Wither = FALCON_EFFECT_WITHER,
+ HealthBoost = FALCON_EFFECT_HEALTH_BOOST,
+ Absorption = FALCON_EFFECT_ABSORPTION,
+ Saturation = FALCON_EFFECT_SATURATION,
+ Levitation = FALCON_EFFECT_LEVITATION,
+ FatalPoison = FALCON_EFFECT_FATAL_POISON,
+ ConduitPower = FALCON_EFFECT_CONDUIT_POWER,
+ SlowFalling = FALCON_EFFECT_SLOW_FALLING,
+ BadOmen = FALCON_EFFECT_BAD_OMEN,
+ VillageHero = FALCON_EFFECT_VILLAGE_HERO,
+ Darkness = FALCON_EFFECT_DARKNESS,
+ Weaving = FALCON_EFFECT_WEAVING,
+ RaidOmen = FALCON_EFFECT_RAID_OMEN,
+ Freezing = FALCON_EFFECT_FREEZING
+ };
+
+ struct Effect {
+ static constexpr int32_t INFINITE = FALCON_EFFECT_INFINITE;
+
+ EffectType type = EffectType::Speed;
+ /** Ticks left, INFINITE for an endless effect. */
+ int32_t duration = 0;
+ int32_t amplifier = 0;
+ bool showParticles = true;
+ bool ambient = false;
+
+ static Effect from(const FalconEffect &value) {
+ return Effect{static_cast(value.type), value.duration, value.amplifier,
+ value.showParticles != 0, value.ambient != 0};
+ }
+ };
+
+ struct Attribute {
+ std::string name;
+ float value = 0.0f;
+ float minimum = 0.0f;
+ float maximum = 0.0f;
+ float defaultValue = 0.0f;
+ };
+}
diff --git a/native/include/falcon/Entity.hpp b/native/include/falcon/Entity.hpp
index f64c198..e9430cb 100644
--- a/native/include/falcon/Entity.hpp
+++ b/native/include/falcon/Entity.hpp
@@ -1,11 +1,13 @@
#pragma once
#include "falcon/Api.hpp"
+#include "falcon/Effects.hpp"
#include "falcon/Vec3.hpp"
#include
#include
#include
+#include
namespace falcon {
class Level;
@@ -115,6 +117,73 @@ namespace falcon {
setOnFire(0);
}
+ /**
+ * Adds a potion effect for duration ticks, or Effect::INFINITE. A weaker or shorter effect than the one
+ * running is refused, as in the game.
+ */
+ bool addEffect(EffectType type, int32_t duration, int32_t amplifier = 0, bool showParticles = true) const {
+ return detail::api().entityAddEffect(mHandle, static_cast(type), duration, amplifier,
+ showParticles ? 1 : 0) != 0;
+ }
+
+ bool removeEffect(EffectType type) const {
+ return detail::api().entityRemoveEffect(mHandle, static_cast(type)) != 0;
+ }
+
+ std::optional effect(EffectType type) const {
+ FalconEffect value{};
+ if (detail::api().entityEffect(mHandle, static_cast(type), &value) == 0)
+ return std::nullopt;
+ return Effect::from(value);
+ }
+
+ bool hasEffect(EffectType type) const {
+ return effect(type).has_value();
+ }
+
+ std::vector effects() const {
+ std::vector result;
+ const uint32_t count = detail::api().entityEffectCount(mHandle);
+ for (uint32_t index = 0; index < count; ++index) {
+ FalconEffect value{};
+ if (detail::api().entityEffectAt(mHandle, index, &value) != 0)
+ result.push_back(Effect::from(value));
+ }
+ return result;
+ }
+
+ void clearEffects() const {
+ detail::api().entityClearEffects(mHandle);
+ }
+
+ /**
+ * An attribute by its identifier, such as minecraft:movement or minecraft:health. Only the attributes the
+ * entity has are found: attributes() lists them.
+ */
+ std::optional attribute(const std::string &name) const {
+ FalconAttribute value{};
+ if (detail::api().entityAttribute(mHandle, name.c_str(), &value) == 0)
+ return std::nullopt;
+ return Attribute{name, value.value, value.minimum, value.maximum, value.defaultValue};
+ }
+
+ std::vector attributes() const {
+ std::vector result;
+ const uint32_t count = detail::api().entityAttributeCount(mHandle);
+ for (uint32_t index = 0; index < count; ++index) {
+ const std::string name = detail::text(detail::api().entityAttributeName(mHandle, index));
+ std::optional found = attribute(name);
+ if (found.has_value())
+ result.push_back(*found);
+ }
+ return result;
+ }
+
+ /** False when the entity has no such attribute or the value is outside its range. */
+ bool setAttribute(const std::string &name, float value) const {
+ return detail::api().entitySetAttribute(mHandle, name.c_str(), value) != 0;
+ }
+
std::optional asPlayer() const;
FalconEntity *handle() const {
diff --git a/native/include/falcon/Falcon.hpp b/native/include/falcon/Falcon.hpp
index c5401de..79189f2 100644
--- a/native/include/falcon/Falcon.hpp
+++ b/native/include/falcon/Falcon.hpp
@@ -9,6 +9,7 @@
#include "falcon/CustomBlock.hpp"
#include "falcon/CustomEntity.hpp"
#include "falcon/CustomItem.hpp"
+#include "falcon/Effects.hpp"
#include "falcon/Entity.hpp"
#include "falcon/EntityEvents.hpp"
#include "falcon/Events.hpp"
diff --git a/native/include/falcon/Level.hpp b/native/include/falcon/Level.hpp
index cfaba5e..36a03ff 100644
--- a/native/include/falcon/Level.hpp
+++ b/native/include/falcon/Level.hpp
@@ -108,6 +108,15 @@ namespace falcon {
detail::api().levelCreateExplosion(mHandle, position.raw(), power, breakBlocks ? 1 : 0);
}
+ /** Plays a sound for every player in this level; the client fades it with distance. */
+ void playSound(const std::string &sound, const Vec3 &position, float volume = 1.0f, float pitch = 1.0f) const {
+ detail::api().levelPlaySound(mHandle, sound.c_str(), position.raw(), volume, pitch);
+ }
+
+ void spawnParticle(const std::string &particle, const Vec3 &position) const {
+ detail::api().levelSpawnParticle(mHandle, particle.c_str(), position.raw());
+ }
+
std::vector entities() const;
FalconLevel *handle() const {
diff --git a/native/include/falcon/Player.hpp b/native/include/falcon/Player.hpp
index ca4e437..8af9cf6 100644
--- a/native/include/falcon/Player.hpp
+++ b/native/include/falcon/Player.hpp
@@ -2,6 +2,7 @@
#include "falcon/Api.hpp"
#include "falcon/PlayerInventory.hpp"
+#include "falcon/Vec3.hpp"
#include
#include
@@ -123,6 +124,21 @@ namespace falcon {
detail::api().playerSendActionBar(mHandle, message.c_str());
}
+ /** Plays a sound only this player hears, from a position in its level. */
+ void playSound(const std::string &sound, const Vec3 &position, float volume = 1.0f, float pitch = 1.0f) const {
+ detail::api().playerPlaySound(mHandle, sound.c_str(), position.raw(), volume, pitch);
+ }
+
+ /** An empty name stops every sound. */
+ void stopSound(const std::string &sound = "") const {
+ detail::api().playerStopSound(mHandle, detail::nullable(sound));
+ }
+
+ /** Shows a particle effect only this player sees, in its level. */
+ void spawnParticle(const std::string &particle, const Vec3 &position) const {
+ detail::api().playerSpawnParticle(mHandle, particle.c_str(), position.raw());
+ }
+
float food() const {
return detail::api().playerFood(mHandle);
}
diff --git a/native/include/falcon/falcon_api.h b/native/include/falcon/falcon_api.h
index 59e8550..b424b64 100644
--- a/native/include/falcon/falcon_api.h
+++ b/native/include/falcon/falcon_api.h
@@ -15,7 +15,7 @@ extern "C" {
#define FALCON_API_VERSION_MAJOR 1
#define FALCON_API_VERSION_MINOR 0
-#define FALCON_API_VERSION_PATCH 5
+#define FALCON_API_VERSION_PATCH 6
#define FALCON_PLUGIN_ENTRY_NAME "falcon_plugin_entry"
typedef struct FalconPlugin FalconPlugin;
@@ -146,6 +146,43 @@ typedef uint32_t FalconImageFit;
#define FALCON_MAP_SIZE 128u
+typedef uint32_t FalconEffectType;
+#define FALCON_EFFECT_SPEED 1u
+#define FALCON_EFFECT_SLOWNESS 2u
+#define FALCON_EFFECT_HASTE 3u
+#define FALCON_EFFECT_MINING_FATIGUE 4u
+#define FALCON_EFFECT_STRENGTH 5u
+#define FALCON_EFFECT_INSTANT_HEALTH 6u
+#define FALCON_EFFECT_INSTANT_DAMAGE 7u
+#define FALCON_EFFECT_JUMP_BOOST 8u
+#define FALCON_EFFECT_NAUSEA 9u
+#define FALCON_EFFECT_REGENERATION 10u
+#define FALCON_EFFECT_RESISTANCE 11u
+#define FALCON_EFFECT_FIRE_RESISTANCE 12u
+#define FALCON_EFFECT_WATER_BREATHING 13u
+#define FALCON_EFFECT_INVISIBILITY 14u
+#define FALCON_EFFECT_BLINDNESS 15u
+#define FALCON_EFFECT_NIGHT_VISION 16u
+#define FALCON_EFFECT_HUNGER 17u
+#define FALCON_EFFECT_WEAKNESS 18u
+#define FALCON_EFFECT_POISON 19u
+#define FALCON_EFFECT_WITHER 20u
+#define FALCON_EFFECT_HEALTH_BOOST 21u
+#define FALCON_EFFECT_ABSORPTION 22u
+#define FALCON_EFFECT_SATURATION 23u
+#define FALCON_EFFECT_LEVITATION 24u
+#define FALCON_EFFECT_FATAL_POISON 25u
+#define FALCON_EFFECT_CONDUIT_POWER 26u
+#define FALCON_EFFECT_SLOW_FALLING 27u
+#define FALCON_EFFECT_BAD_OMEN 28u
+#define FALCON_EFFECT_VILLAGE_HERO 29u
+#define FALCON_EFFECT_DARKNESS 30u
+#define FALCON_EFFECT_WEAVING 33u
+#define FALCON_EFFECT_RAID_OMEN 36u
+#define FALCON_EFFECT_FREEZING 38u
+
+#define FALCON_EFFECT_INFINITE -1
+
typedef uint32_t FalconLogLevel;
#define FALCON_LOG_INFO 0u
#define FALCON_LOG_WARNING 1u
@@ -225,6 +262,22 @@ typedef struct FalconPluginCallbacks {
void *userData;
} FalconPluginCallbacks;
+typedef struct FalconEffect {
+ FalconEffectType type;
+ /* ticks left, FALCON_EFFECT_INFINITE for an endless effect */
+ int32_t duration;
+ int32_t amplifier;
+ int32_t showParticles;
+ int32_t ambient;
+} FalconEffect;
+
+typedef struct FalconAttribute {
+ float value;
+ float minimum;
+ float maximum;
+ float defaultValue;
+} FalconAttribute;
+
typedef struct FalconServerApi {
uint32_t size;
uint32_t versionMajor;
@@ -460,6 +513,23 @@ typedef struct FalconServerApi {
int (*playerGiveMap)(FalconPlayer *player, int64_t mapId);
const char *(*mapLastError)(void);
int (*mapDeleteImage)(FalconLevel *level, int64_t mapId);
+
+ int (*entityAddEffect)(FalconEntity *entity, FalconEffectType type, int32_t duration, int32_t amplifier,
+ int showParticles);
+ int (*entityRemoveEffect)(FalconEntity *entity, FalconEffectType type);
+ int (*entityEffect)(FalconEntity *entity, FalconEffectType type, FalconEffect *out);
+ uint32_t (*entityEffectCount)(FalconEntity *entity);
+ int (*entityEffectAt)(FalconEntity *entity, uint32_t index, FalconEffect *out);
+ void (*entityClearEffects)(FalconEntity *entity);
+ uint32_t (*entityAttributeCount)(FalconEntity *entity);
+ const char *(*entityAttributeName)(FalconEntity *entity, uint32_t index);
+ int (*entityAttribute)(FalconEntity *entity, const char *name, FalconAttribute *out);
+ int (*entitySetAttribute)(FalconEntity *entity, const char *name, float value);
+ void (*playerPlaySound)(FalconPlayer *player, const char *sound, FalconVec3 position, float volume, float pitch);
+ void (*levelPlaySound)(FalconLevel *level, const char *sound, FalconVec3 position, float volume, float pitch);
+ 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);
} FalconServerApi;
typedef int (*FalconPluginEntry)(const FalconServerApi *api, FalconPlugin *plugin, FalconPluginCallbacks *callbacks);
diff --git a/wasm/falcon.wit b/wasm/falcon.wit
index 2946da8..c58f38d 100644
--- a/wasm/falcon.wit
+++ b/wasm/falcon.wit
@@ -1,6 +1,6 @@
/// The Falcon plugin API for WebAssembly plugins.
///
-/// Every imported function mirrors one entry of the C API table in `falcon_api.h` (API 1.0.5) and calls it,
+/// Every imported function mirrors one entry of the C API table in `falcon_api.h` (API 1.0.6) and calls it,
/// so a Wasm plugin reaches exactly what a native plugin reaches. The only C entry without a counterpart is
/// `runAsync`: a plugin instance is not thread safe, so it cannot run on the scheduler's worker thread.
///
@@ -8,7 +8,7 @@
/// a stale id is simply not found. Levels and items are host-side handles that are checked on every call.
/// Events and command senders have no handle: `event` and the sender functions of `commands` act on the
/// event or command being handled and return default values outside a handler.
-package falcon:plugin@1.0.5;
+package falcon:plugin@1.0.6;
interface types {
/// The runtime id of a player. A player is also an entity, so the same id works with `entities`.
@@ -76,6 +76,25 @@ interface types {
feet,
}
+ /// One of the `FALCON_EFFECT_*` values.
+ type effect-type = u32;
+
+ /// `duration` is in ticks, -1 for an endless effect.
+ record effect {
+ effect-type: effect-type,
+ duration: s32,
+ amplifier: s32,
+ show-particles: bool,
+ ambient: bool,
+ }
+
+ record attribute {
+ value: f32,
+ minimum: f32,
+ maximum: f32,
+ default-value: f32,
+ }
+
/// A level of a loaded world. A level whose world was unloaded behaves like a missing one.
resource level;
@@ -99,7 +118,7 @@ interface server {
}
interface players {
- use types.{player, entity, game-mode};
+ use types.{player, entity, game-mode, vec3};
player-name: func(player: player) -> string;
player-is-operator: func(player: player) -> bool;
@@ -126,10 +145,14 @@ interface players {
player-transfer: func(player: player, address: string, port: u32) -> bool;
player-protocol-version: func(player: player) -> s32;
player-send-packet: func(player: player, packet-id: u32, data: list) -> bool;
+ player-play-sound: func(player: player, sound: string, position: vec3, volume: f32, pitch: f32);
+ /// `none` stops every sound.
+ player-stop-sound: func(player: player, sound: option);
+ player-spawn-particle: func(player: player, particle: string, position: vec3);
}
interface entities {
- use types.{entity, level, vec3};
+ use types.{entity, level, vec3, effect-type, effect, attribute};
entity-type: func(entity: entity) -> string;
entity-runtime-id: func(entity: entity) -> u64;
@@ -154,6 +177,17 @@ interface entities {
entity-set-invisible: func(entity: entity, invisible: bool);
level-entity-count: func(level: borrow) -> u32;
level-entity: func(level: borrow, index: u32) -> option;
+ entity-add-effect: func(entity: entity, effect-type: effect-type, duration: s32, amplifier: s32,
+ show-particles: bool) -> bool;
+ entity-remove-effect: func(entity: entity, effect-type: effect-type) -> bool;
+ entity-effect: func(entity: entity, effect-type: effect-type) -> option;
+ entity-effect-count: func(entity: entity) -> u32;
+ entity-effect-at: func(entity: entity, index: u32) -> option;
+ entity-clear-effects: func(entity: entity);
+ entity-attribute-count: func(entity: entity) -> u32;
+ 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;
}
interface levels {
@@ -185,6 +219,8 @@ interface levels {
world-level: func(name: string, dimension: dimension) -> option;
server-load-world: func(name: string, create: bool, seed: option) -> bool;
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);
}
interface items {