diff --git a/README.md b/README.md index 06a0e8e..f629bca 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

CI - API + API Languages Platform

@@ -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 {