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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,27 @@ falcon::Recipes::addShapeless("myplugin:dye", {"minecraft:dandelion"}, "minecraf
falcon::Recipes::remove("myplugin:dye");
```

### World

Levels expose the rules and state of their world: game rules, the biome at a position, rays, entities in a box,
structures and ticking areas. The difficulty belongs to the server:

```cpp
level.setGameRule("keepinventory", "true");
std::string biome = level.biome({0, 64, 0});

const falcon::Entity eyes = player.entity();
if (std::optional<falcon::BlockHit> hit = level.raycastBlock(eyes.position(), {0, -1, 0}, 16.0))
player.sendMessage("standing over " + level.blockName(hit->block));

for (const falcon::Entity &nearby: level.entitiesInBox({-8, 60, -8}, {8, 70, 8}))
nearby.setOnFire(60);

level.placeStructure("mystructure:house", {100, 64, 100}, 1);
level.addTickingArea("spawn", {-16, 0, -16}, {16, 0, 16});
falcon::Server::setDifficulty(falcon::Difficulty::Hard);
```

### C# plugins

The `dotnet` runtime is deprecated. Write new plugins for Wasm, native C++ or Java.
Expand Down
31 changes: 31 additions & 0 deletions java/src/main/java/falcon/api/BlockFace.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package falcon.api;

import falcon.api.internal.FalconAbi;

public enum BlockFace {
DOWN(FalconAbi.FACE_DOWN),
UP(FalconAbi.FACE_UP),
NORTH(FalconAbi.FACE_NORTH),
SOUTH(FalconAbi.FACE_SOUTH),
WEST(FalconAbi.FACE_WEST),
EAST(FalconAbi.FACE_EAST);

private final int mValue;

BlockFace(int value) {
mValue = value;
}

public int value() {
return mValue;
}

public static BlockFace of(int value) {
for (BlockFace entry : values()) {
if (entry.mValue == value) {
return entry;
}
}
throw new IllegalArgumentException("Unknown value " + value);
}
}
7 changes: 7 additions & 0 deletions java/src/main/java/falcon/api/BlockHit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package falcon.api;

/**
* The block a ray hit; position is where the ray enters it.
*/
public record BlockHit(BlockPos block, BlockFace face, Vec3 position, double distance) {
}
29 changes: 29 additions & 0 deletions java/src/main/java/falcon/api/Difficulty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package falcon.api;

import falcon.api.internal.FalconAbi;

public enum Difficulty {
PEACEFUL(FalconAbi.DIFFICULTY_PEACEFUL),
EASY(FalconAbi.DIFFICULTY_EASY),
NORMAL(FalconAbi.DIFFICULTY_NORMAL),
HARD(FalconAbi.DIFFICULTY_HARD);

private final int mValue;

Difficulty(int value) {
mValue = value;
}

public int value() {
return mValue;
}

public static Difficulty of(int value) {
for (Difficulty entry : values()) {
if (entry.mValue == value) {
return entry;
}
}
throw new IllegalArgumentException("Unknown value " + value);
}
}
4 changes: 4 additions & 0 deletions java/src/main/java/falcon/api/EntityHit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package falcon.api;

public record EntityHit(Entity entity, double distance) {
}
134 changes: 134 additions & 0 deletions java/src/main/java/falcon/api/Level.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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.lang.foreign.ValueLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -215,6 +217,138 @@ public boolean setSignText(BlockPos position, String text, SignSide side) {
}
}

public List<String> gameRules() {
int count = api().levelGameRuleCount(mHandle);
List<String> names = new ArrayList<>(Math.max(count, 0));
for (int index = 0; index < count; index++) {
names.add(Interop.string(api().levelGameRuleName(mHandle, index)));
}
return names;
}

/**
* "true" or "false" for a switch, the number otherwise; the rules belong to the level's world.
*/
public Optional<String> gameRule(String name) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment value = api().levelGameRule(mHandle, Interop.text(arena, name));
return Interop.isNull(value) ? Optional.empty() : Optional.of(Interop.string(value));
}
}

public boolean setGameRule(String name, String value) {
try (Arena arena = Arena.ofConfined()) {
return api().levelSetGameRule(mHandle, Interop.text(arena, name), Interop.text(arena, value)) != 0;
}
}

/**
* The biome identifier at a position, such as minecraft:plains.
*/
public String biome(BlockPos position) {
try (Arena arena = Arena.ofConfined()) {
return Interop.string(api().levelBiome(mHandle, Interop.blockPos(arena, position)));
}
}

/**
* The first block on the ray; an unloaded chunk stops it without a hit.
*/
public Optional<BlockHit> raycastBlock(Vec3 origin, Vec3 direction, double maxDistance, boolean includeLiquids,
boolean includePassable) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment hit = arena.allocate(FalconAbi.BlockHit.LAYOUT);
if (api().levelRaycastBlock(mHandle, Interop.vec3(arena, origin), Interop.vec3(arena, direction),
maxDistance, includeLiquids ? 1 : 0, includePassable ? 1 : 0, hit) == 0) {
return Optional.empty();
}
BlockPos block = Interop.blockPos(hit.asSlice(FalconAbi.BlockHit.BLOCK, FalconAbi.BlockPos.LAYOUT));
Vec3 position = Interop.vec3(hit.asSlice(FalconAbi.BlockHit.POSITION, FalconAbi.Vec3.LAYOUT));
return Optional.of(new BlockHit(block, BlockFace.of(hit.get(ValueLayout.JAVA_INT, FalconAbi.BlockHit.FACE)),
position, hit.get(ValueLayout.JAVA_DOUBLE, FalconAbi.BlockHit.DISTANCE)));
}
}

/**
* The nearest entity on the ray, before any block; except, usually the one looking, may be null.
*/
public Optional<EntityHit> raycastEntity(Vec3 origin, Vec3 direction, double maxDistance, Entity except) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment distance = arena.allocate(ValueLayout.JAVA_DOUBLE);
MemorySegment hit = api().levelRaycastEntity(mHandle, Interop.vec3(arena, origin),
Interop.vec3(arena, direction), maxDistance, except == null ? MemorySegment.NULL : except.handle(),
distance);
if (Interop.isNull(hit)) {
return Optional.empty();
}
return Optional.of(new EntityHit(new Entity(mContext, hit), distance.get(ValueLayout.JAVA_DOUBLE, 0)));
}
}

/**
* Every entity and player whose box touches the box between two corners.
*/
public List<Entity> entitiesInBox(Vec3 from, Vec3 to) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment low = Interop.vec3(arena, from);
MemorySegment high = Interop.vec3(arena, to);
int capacity = 64;
MemorySegment handles = arena.allocate(ValueLayout.ADDRESS, capacity);
int count = api().levelEntitiesInBox(mHandle, low, high, handles, capacity);
if (count > capacity) {
capacity = count;
handles = arena.allocate(ValueLayout.ADDRESS, capacity);
count = api().levelEntitiesInBox(mHandle, low, high, handles, capacity);
}
List<Entity> result = new ArrayList<>(Math.max(count, 0));
for (int index = 0; index < Math.min(count, capacity); index++) {
result.add(new Entity(mContext, handles.getAtIndex(ValueLayout.ADDRESS, index)));
}
return result;
}
}

public boolean placeStructure(String structure, BlockPos position) {
return placeStructure(structure, position, 0, Mirror.NONE, true, 1.0f);
}

/**
* Places a saved structure by identifier with its lowest corner at position. Rotation is in quarter turns
* clockwise; integrity keeps that share of blocks.
*/
public boolean placeStructure(String structure, BlockPos position, int rotation, Mirror mirror,
boolean includeEntities, float integrity) {
try (Arena arena = Arena.ofConfined()) {
return api().levelPlaceStructure(mHandle, Interop.text(arena, structure), Interop.blockPos(arena, position),
rotation, mirror.value(), includeEntities ? 1 : 0, integrity) != 0;
}
}

/**
* Keeps the chunks between two corners loaded and ticking, with the limits of /tickingarea.
*/
public boolean addTickingArea(String name, BlockPos from, BlockPos to, boolean preload) {
try (Arena arena = Arena.ofConfined()) {
return api().levelAddTickingArea(mHandle, Interop.text(arena, name), Interop.blockPos(arena, from),
Interop.blockPos(arena, to), preload ? 1 : 0) != 0;
}
}

public boolean removeTickingArea(String name) {
try (Arena arena = Arena.ofConfined()) {
return api().levelRemoveTickingArea(mHandle, Interop.text(arena, name)) != 0;
}
}

public List<String> tickingAreas() {
int count = api().levelTickingAreaCount(mHandle);
List<String> names = new ArrayList<>(Math.max(count, 0));
for (int index = 0; index < count; index++) {
names.add(Interop.string(api().levelTickingAreaName(mHandle, index)));
}
return names;
}

public List<Entity> entities() {
int count = api().levelEntityCount(mHandle);
List<Entity> result = new ArrayList<>(count);
Expand Down
29 changes: 29 additions & 0 deletions java/src/main/java/falcon/api/Mirror.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package falcon.api;

import falcon.api.internal.FalconAbi;

public enum Mirror {
NONE(FalconAbi.MIRROR_NONE),
X(FalconAbi.MIRROR_X),
Z(FalconAbi.MIRROR_Z),
XZ(FalconAbi.MIRROR_XZ);

private final int mValue;

Mirror(int value) {
mValue = value;
}

public int value() {
return mValue;
}

public static Mirror of(int value) {
for (Mirror entry : values()) {
if (entry.mValue == value) {
return entry;
}
}
throw new IllegalArgumentException("Unknown value " + value);
}
}
11 changes: 11 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,17 @@ public boolean unloadWorld(String name) {
}
}

public Difficulty difficulty() {
return Difficulty.of(api().serverDifficulty());
}

/**
* Saved in the server properties and sent to every player, like /difficulty.
*/
public boolean setDifficulty(Difficulty difficulty) {
return api().serverSetDifficulty(difficulty.value()) != 0;
}

public Scoreboard scoreboard() {
return new Scoreboard(mContext);
}
Expand Down
Loading
Loading