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
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<p align="center">
<a href="https://github.com/Falcon-MC/PluginAPI/actions/workflows/ci.yml"><img src="https://github.com/Falcon-MC/PluginAPI/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/api-v1.0.5-blue" alt="API">
<img src="https://img.shields.io/badge/api-v1.0.6-blue" alt="API">
<img src="https://img.shields.io/badge/language-C%2B%2B17%20%7C%20C%23%20%7C%20Java%20%7C%20Wasm-00599C" alt="Languages">
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey" alt="Platform">
</p>
Expand Down Expand Up @@ -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<falcon::Attribute> 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.
Expand All @@ -137,7 +157,7 @@ A C# plugin is a `net8.0` class library that references `Falcon.PluginAPI` and s
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Falcon.PluginAPI" Version="1.0.5" />
<PackageReference Include="Falcon.PluginAPI" Version="1.0.6" />
</ItemGroup>
</Project>
```
Expand Down Expand Up @@ -193,7 +213,7 @@ A Java plugin is a Java 22 jar that depends on `falcon-plugin-api` with the `pro
<dependency>
<groupId>io.github.falcon-mc</groupId>
<artifactId>falcon-plugin-api</artifactId>
<version>1.0.5</version>
<version>1.0.6</version>
<scope>provided</scope>
</dependency>
```
Expand Down Expand Up @@ -222,7 +242,7 @@ full class name and `jar` defaults to `<name>.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"
Expand Down
2 changes: 1 addition & 1 deletion examples/java/HelloPlugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion examples/java/HelloPlugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<properties>
<maven.compiler.release>22</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<falcon.api.version>1.0.5</falcon.api.version>
<falcon.api.version>1.0.6</falcon.api.version>
</properties>

<dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 -> {
Expand Down
5 changes: 5 additions & 0 deletions examples/native/HelloPlugin/src/Listeners/JoinListener.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<falcon::PlayerQuitEvent>([&plugin](falcon::PlayerQuitEvent &event) {
Expand Down
2 changes: 1 addition & 1 deletion java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.falcon-mc</groupId>
<artifactId>falcon-plugin-api</artifactId>
<version>1.0.5</version>
<version>1.0.6</version>
<packaging>jar</packaging>

<name>Falcon Plugin API</name>
Expand Down
15 changes: 15 additions & 0 deletions java/src/main/java/falcon/api/Attribute.java
Original file line number Diff line number Diff line change
@@ -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));
}
}
21 changes: 21 additions & 0 deletions java/src/main/java/falcon/api/Effect.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
58 changes: 58 additions & 0 deletions java/src/main/java/falcon/api/EffectType.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
83 changes: 83 additions & 0 deletions java/src/main/java/falcon/api/Entity.java
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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> 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<Effect> effects() {
int count = api().entityEffectCount(mHandle);
List<Effect> 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> 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<Attribute> attributes() {
int count = api().entityAttributeCount(mHandle);
List<Attribute> 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<Player> asPlayer() {
MemorySegment player = api().entityPlayer(mHandle);
if (Interop.isNull(player)) {
Expand Down
19 changes: 19 additions & 0 deletions java/src/main/java/falcon/api/Level.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Entity> entities() {
int count = api().levelEntityCount(mHandle);
List<Entity> result = new ArrayList<>(count);
Expand Down
32 changes: 32 additions & 0 deletions java/src/main/java/falcon/api/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading
Loading