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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,31 @@ Java plugins reach the same store through `plugin.data()`. `Item::nbt` and `Item
item's whole tag as little-endian NBT, the game's own format, and `Entity::nbt` returns what the server would save
for an entity.

### Typed commands, running commands and recipes

An overload gives a command typed parameters that the client shows and completes; enum parameters list their
choices. The handler still gets its arguments as text. Commands run as the console or as a player:

```cpp
commands().add("warp", "Teleports to a warp", "/warp <name>", handler, falcon::CommandPermission::Any);
commands().addOverload("warp", {{"name", falcon::CommandParamType::Enum, false, {"spawn", "shop", "arena"}},
{"target", falcon::CommandParamType::Target, true}});

commands().execute("time set day");
player.executeCommand("me waves");
```

Recipes are crafting table recipes, shaped or shapeless; players online get the new recipe book at once. A vanilla
crafting recipe is removed by its identifier until the server restarts:

```cpp
falcon::Recipes::addShaped("myplugin:emerald_block", 2, 2,
{"minecraft:emerald", "minecraft:emerald", "minecraft:emerald", "minecraft:emerald"},
"minecraft:emerald_block");
falcon::Recipes::addShapeless("myplugin:dye", {"minecraft:dandelion"}, "minecraft:yellow_dye", 2);
falcon::Recipes::remove("myplugin:dye");
```

### 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
Expand Up @@ -6,6 +6,8 @@
import falcon.api.Player;
import falcon.api.Plugin;
import falcon.api.command.CommandContext;
import falcon.api.command.CommandParamType;
import falcon.api.command.CommandParameter;
import falcon.api.command.CommandPermission;

import java.util.List;
Expand All @@ -22,6 +24,10 @@ public static void registerTo(Plugin plugin) {
plugin.permissions().add(PERMISSION, PermissionDefault.OPERATOR);
plugin.commands().add("give-diamond", "Gives you a shiny diamond", "/give-diamond [count]",
GiveDiamondCommand::execute, CommandPermission.ANY);
plugin.commands().addOverload("give-diamond",
List.of(CommandParameter.optional("count", CommandParamType.INT)));
plugin.server().recipes().addShapeless("hello:diamond_from_coal", List.of("minecraft:coal", "minecraft:coal",
"minecraft:coal", "minecraft:coal"), DIAMOND, 1);
}

private static boolean execute(CommandContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ void GiveDiamondCommand::registerTo(const falcon::Plugin &plugin) {
context.reply("Here you go!");
return true;
}, falcon::CommandPermission::Any);
plugin.commands().addOverload("give-diamond", {{"count", falcon::CommandParamType::Int, true}});
falcon::Recipes::addShapeless("hello:diamond_from_coal",
{"minecraft:coal", "minecraft:coal", "minecraft:coal", "minecraft:coal"}, DIAMOND);
}
9 changes: 9 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,15 @@ public void sendActionBar(String message) {
}
}

/**
* Runs a command line, without the slash, as this player, with the player's permissions.
*/
public boolean executeCommand(String commandLine) {
try (Arena arena = Arena.ofConfined()) {
return api().playerExecuteCommand(mHandle, Interop.text(arena, commandLine)) != 0;
}
}

public void playSound(String sound, Vec3 position) {
playSound(sound, position, 1.0f, 1.0f);
}
Expand Down
69 changes: 69 additions & 0 deletions java/src/main/java/falcon/api/Recipes.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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.List;

/**
* Crafting table recipes. Players online get the new recipe book at once. A removed vanilla recipe is gone until the
* server restarts.
*/
public final class Recipes {
private final PluginContext mContext;

Recipes(PluginContext context) {
mContext = context;
}

/**
* Ingredients row by row, width x height of them; an empty string leaves the cell empty.
*/
public boolean addShaped(String identifier, int width, int height, List<String> ingredients, String result,
int count) {
return add(identifier, true, width, height, ingredients, result, count);
}

public boolean addShapeless(String identifier, List<String> ingredients, String result, int count) {
return add(identifier, false, 0, 0, ingredients, result, count);
}

/**
* Removes a recipe added by a plugin or a pack, or a vanilla crafting recipe by its identifier.
*/
public boolean remove(String identifier) {
try (Arena arena = Arena.ofConfined()) {
return api().removeRecipe(Interop.text(arena, identifier)) != 0;
}
}

private boolean add(String identifier, boolean shaped, int width, int height, List<String> ingredients,
String result, int count) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment items = arena.allocate(ValueLayout.ADDRESS, Math.max(ingredients.size(), 1));
for (int index = 0; index < ingredients.size(); index++) {
items.setAtIndex(ValueLayout.ADDRESS, index, Interop.text(arena, ingredients.get(index)));
}

MemorySegment descriptor = arena.allocate(FalconAbi.RecipeDescriptor.LAYOUT);
descriptor.set(ValueLayout.ADDRESS, FalconAbi.RecipeDescriptor.IDENTIFIER, Interop.text(arena, identifier));
descriptor.set(ValueLayout.JAVA_INT, FalconAbi.RecipeDescriptor.SHAPED, shaped ? 1 : 0);
descriptor.set(ValueLayout.JAVA_INT, FalconAbi.RecipeDescriptor.WIDTH, width);
descriptor.set(ValueLayout.JAVA_INT, FalconAbi.RecipeDescriptor.HEIGHT, height);
descriptor.set(ValueLayout.ADDRESS, FalconAbi.RecipeDescriptor.INGREDIENTS, items);
descriptor.set(ValueLayout.JAVA_INT, FalconAbi.RecipeDescriptor.INGREDIENT_COUNT, ingredients.size());
descriptor.set(ValueLayout.ADDRESS, FalconAbi.RecipeDescriptor.RESULT, Interop.text(arena, result));
descriptor.set(ValueLayout.JAVA_INT, FalconAbi.RecipeDescriptor.RESULT_COUNT, count);
return api().registerRecipe(mContext.handle(), descriptor) != 0;
}
}

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 @@ -101,6 +101,10 @@ public Scoreboard scoreboard() {
return new Scoreboard(mContext);
}

public Recipes recipes() {
return new Recipes(mContext);
}

private static NativeApi api() {
return Interop.api();
}
Expand Down
26 changes: 26 additions & 0 deletions java/src/main/java/falcon/api/command/CommandParamType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package falcon.api.command;

import falcon.api.internal.FalconAbi;

public enum CommandParamType {
INT(FalconAbi.COMMAND_PARAM_INT),
FLOAT(FalconAbi.COMMAND_PARAM_FLOAT),
STRING(FalconAbi.COMMAND_PARAM_STRING),
BOOL(FalconAbi.COMMAND_PARAM_BOOL),
TARGET(FalconAbi.COMMAND_PARAM_TARGET),
POSITION(FalconAbi.COMMAND_PARAM_POSITION),
BLOCK_POSITION(FalconAbi.COMMAND_PARAM_BLOCK_POSITION),
MESSAGE(FalconAbi.COMMAND_PARAM_MESSAGE),
JSON(FalconAbi.COMMAND_PARAM_JSON),
ENUM(FalconAbi.COMMAND_PARAM_ENUM);

private final int mValue;

CommandParamType(int value) {
mValue = value;
}

public int value() {
return mValue;
}
}
20 changes: 20 additions & 0 deletions java/src/main/java/falcon/api/command/CommandParameter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package falcon.api.command;

import java.util.List;

/**
* One parameter of a command overload. The values of an enum parameter are the choices the client completes.
*/
public record CommandParameter(String name, CommandParamType type, boolean optional, List<String> values) {
public static CommandParameter of(String name, CommandParamType type) {
return new CommandParameter(name, type, false, List.of());
}

public static CommandParameter optional(String name, CommandParamType type) {
return new CommandParameter(name, type, true, List.of());
}

public static CommandParameter choice(String name, List<String> values) {
return new CommandParameter(name, CommandParamType.ENUM, false, List.copyOf(values));
}
}
35 changes: 35 additions & 0 deletions java/src/main/java/falcon/api/command/Commands.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,41 @@ public boolean add(String name, String description, String usage, CommandHandler
}
}

/**
* Adds a typed signature to a command of this plugin; the client shows it and completes its arguments. The handler
* still receives the arguments as text, one per word. The first overload replaces the default one.
*/
public boolean addOverload(String command, List<CommandParameter> parameters) {
try (Arena arena = Arena.ofConfined()) {
long size = FalconAbi.CommandParameter.LAYOUT.byteSize();
MemorySegment array = arena.allocate(FalconAbi.CommandParameter.LAYOUT, Math.max(parameters.size(), 1));
for (int index = 0; index < parameters.size(); index++) {
CommandParameter parameter = parameters.get(index);
MemorySegment entry = array.asSlice(index * size, size);
MemorySegment values = arena.allocate(ValueLayout.ADDRESS, Math.max(parameter.values().size(), 1));
for (int value = 0; value < parameter.values().size(); value++) {
values.setAtIndex(ValueLayout.ADDRESS, value, Interop.text(arena, parameter.values().get(value)));
}
entry.set(ValueLayout.ADDRESS, FalconAbi.CommandParameter.NAME, Interop.text(arena, parameter.name()));
entry.set(ValueLayout.JAVA_INT, FalconAbi.CommandParameter.TYPE, parameter.type().value());
entry.set(ValueLayout.JAVA_INT, FalconAbi.CommandParameter.OPTIONAL, parameter.optional() ? 1 : 0);
entry.set(ValueLayout.ADDRESS, FalconAbi.CommandParameter.VALUES, values);
entry.set(ValueLayout.JAVA_INT, FalconAbi.CommandParameter.VALUE_COUNT, parameter.values().size());
}
return Interop.api().commandAddOverload(mContext.handle(), Interop.text(arena, command), array,
parameters.size()) != 0;
}
}

/**
* Runs a command line, without the slash, as the server console.
*/
public boolean execute(String commandLine) {
try (Arena arena = Arena.ofConfined()) {
return Interop.api().serverExecuteCommand(Interop.text(arena, commandLine)) != 0;
}
}

private static int dispatch(MemorySegment sender, MemorySegment arguments, int argumentCount,
MemorySegment userData) {
try {
Expand Down
48 changes: 48 additions & 0 deletions native/include/falcon/Commands.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,27 @@ namespace falcon {

using CommandHandler = std::function<bool(CommandContext &)>;

enum class CommandParamType : uint32_t {
Int = FALCON_COMMAND_PARAM_INT,
Float = FALCON_COMMAND_PARAM_FLOAT,
String = FALCON_COMMAND_PARAM_STRING,
Bool = FALCON_COMMAND_PARAM_BOOL,
Target = FALCON_COMMAND_PARAM_TARGET,
Position = FALCON_COMMAND_PARAM_POSITION,
BlockPosition = FALCON_COMMAND_PARAM_BLOCK_POSITION,
Message = FALCON_COMMAND_PARAM_MESSAGE,
Json = FALCON_COMMAND_PARAM_JSON,
Enum = FALCON_COMMAND_PARAM_ENUM
};

struct CommandParameter {
std::string name;
CommandParamType type = CommandParamType::String;
bool optional = false;
/** The choices of an enum parameter, offered by the client's completion. */
std::vector<std::string> values;
};

class Commands {
public:
bool add(const std::string &name, const std::string &description, const std::string &usage,
Expand All @@ -66,6 +87,33 @@ namespace falcon {
return detail::api().registerCommand(detail::plugin(), &descriptor) != 0;
}

/**
* Adds a typed signature to a command of this plugin; the client shows it and completes its arguments. The
* handler still receives the arguments as text, one per word. The first overload replaces the default one.
*/
bool addOverload(const std::string &command, const std::vector<CommandParameter> &parameters) const {
std::vector<std::vector<const char *>> values;
std::vector<FalconCommandParameter> raw;
values.reserve(parameters.size());
raw.reserve(parameters.size());
for (const CommandParameter &parameter: parameters) {
values.emplace_back();
for (const std::string &value: parameter.values)
values.back().push_back(value.c_str());
raw.push_back(FalconCommandParameter{parameter.name.c_str(),
static_cast<FalconCommandParamType>(parameter.type),
parameter.optional ? 1 : 0, values.back().data(),
static_cast<uint32_t>(values.back().size())});
}
return detail::api().commandAddOverload(detail::plugin(), command.c_str(), raw.data(),
static_cast<uint32_t>(raw.size())) != 0;
}

/** Runs a command line, without the slash, as the server console. */
bool execute(const std::string &commandLine) const {
return detail::api().serverExecuteCommand(commandLine.c_str()) != 0;
}

private:
static int dispatch(FalconCommandSender *sender, const char *const *arguments, uint32_t argumentCount,
void *userData) {
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 @@ -24,6 +24,7 @@
#include "falcon/PlayerEvents.hpp"
#include "falcon/PlayerInventory.hpp"
#include "falcon/Plugin.hpp"
#include "falcon/Recipes.hpp"
#include "falcon/Scheduler.hpp"
#include "falcon/Scoreboard.hpp"
#include "falcon/Server.hpp"
Expand Down
5 changes: 5 additions & 0 deletions native/include/falcon/Player.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ namespace falcon {
detail::api().playerSendActionBar(mHandle, message.c_str());
}

/** Runs a command line, without the slash, as this player, with the player's permissions. */
bool executeCommand(const std::string &commandLine) const {
return detail::api().playerExecuteCommand(mHandle, commandLine.c_str()) != 0;
}

/** 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);
Expand Down
46 changes: 46 additions & 0 deletions native/include/falcon/Recipes.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once

#include "falcon/Api.hpp"

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

namespace falcon {
/**
* Crafting table recipes. Players online get the new recipe book at once. A removed vanilla recipe is gone until
* the server restarts.
*/
class Recipes {
public:
/** Ingredients row by row, width x height of them; an empty string leaves the cell empty. */
static bool addShaped(const std::string &identifier, uint32_t width, uint32_t height,
const std::vector<std::string> &ingredients, const std::string &result,
uint32_t count = 1) {
return add(identifier, true, width, height, ingredients, result, count);
}

static bool addShapeless(const std::string &identifier, const std::vector<std::string> &ingredients,
const std::string &result, uint32_t count = 1) {
return add(identifier, false, 0, 0, ingredients, result, count);
}

/** Removes a recipe added by a plugin or a pack, or a vanilla crafting recipe by its identifier. */
static bool remove(const std::string &identifier) {
return detail::api().removeRecipe(identifier.c_str()) != 0;
}

private:
static bool add(const std::string &identifier, bool shaped, uint32_t width, uint32_t height,
const std::vector<std::string> &ingredients, const std::string &result, uint32_t count) {
std::vector<const char *> items;
items.reserve(ingredients.size());
for (const std::string &ingredient: ingredients)
items.push_back(ingredient.c_str());

const FalconRecipeDescriptor descriptor{identifier.c_str(), shaped ? 1 : 0, width, height, items.data(),
static_cast<uint32_t>(items.size()), result.c_str(), count};
return detail::api().registerRecipe(detail::plugin(), &descriptor) != 0;
}
};
}
Loading
Loading