Placeholders
API State: Draft
Unfinished
This part of the API is brand-new. It will be changed if there are any bugs, missing features or usability improvements. This part of the API might receive breaking changes within a minor version.It is not recommended relying on this part of the API, it will most likely change.
Draft
Our own usage and testing has shown that this part of the API is complete and seems bug free. However, other plugins may have other use cases which are not covered by our testing. Therefore, please go ahead and use this API part. Let us know if there are missing features or bugs. This API part will be changed if there are more bugs, missing features or usability improvements. We will try to keep it compatible with previous versions if changes are needed. This part of the API won't receive breaking changes within a minor version.Please use this part of the API and give us feedback!
Stable
Both our own and third party testing showed that this part of the API is complete. Only bugs and major conceptual problems would lead to more changes. This part of the API will only change over at least one major version and will likely carry deprecation as long as possible.This part of the API should be safe to use. We try to keep it compatible with previous versions if changes are needed.
Placeholder API Classes
org.betonquest.betonquest.api.service.placeholder.Placeholdersorg.betonquest.betonquest.api.service.placeholder.PlaceholderManagerorg.betonquest.betonquest.api.service.placeholder.PlaceholderRegistryorg.betonquest.betonquest.api.quest.placeholder.PlayerPlaceholderFactoryorg.betonquest.betonquest.api.quest.placeholder.PlayerlessPlaceholderFactoryorg.betonquest.betonquest.api.quest.placeholder.PlayerPlaceholderorg.betonquest.betonquest.api.quest.placeholder.PlayerlessPlaceholderorg.betonquest.betonquest.api.quest.placeholder.NullablePlaceholderorg.betonquest.betonquest.api.quest.placeholder.OnlinePlaceholder
Introductionπ
This page covers the Placeholder API of BetonQuest.
You should have viewed these pages
What this page covers
What this page does not cover
- What a placeholder is
- Which placeholders are available
- How to work with placeholders in scripting
How to create a placeholderπ
Creating a placeholder will be explained in the following sections using the WeatherPlaceholder placeholder as an example.
Approach #1: A simple implementationπ
The first approach is by far the simplest one. We expect the placeholder to be a simple string representation of the
weather in the world world. Since there is no weather value directly present in the API of paper as of now, we will
need to create a custom implementation for the value. To reduce the amount of boilerplate code in the following examples,
we will extract that value from the world in a separate method.
An example implementation of that method could look like this:
private String getWeather(final World world) {
if(world.isThundering()) {
return "thunder";
}
if(world.hasStorm()) {
return "rain";
}
return "clear";
}
Now, to create a placeholder, we need to implement the PlayerlessPlaceholder interface.
public class WeatherPlaceholder implements PlayerlessPlaceholder {
public WeatherPlaceholder() {
}
@Override
public String getValue() {
final World world = Bukkit.getWorld("world"); //(1)!
return getWeather(world); //(2)!
}
}
- To get the world named
world. - Call the
getWeathermethod to get the weather value and return it.
Approach #2: Using a profileπ
To make the placeholder more useful, we might want to use the world of the player for which the placeholder is being
resolved. For that, we will have to implement the OnlinePlaceholder interface instead of PlayerlessPlaceholder.
public class WeatherPlaceholder implements OnlinePlaceholder {
public WeatherPlaceholder() {
}
@Override
public String getValue(final OnlineProfile onlineProfile) {
final World world = onlineProfile.getPlayer().getWorld(); //(1)!
return getWeather(world); //(2)!
}
}
- To get the world of the player the placeholder is being resolved for.
- Call the
getWeathermethod to get the weather value and return it.
Approach #3: Adding a parameterπ
To increase the versatility of the placeholder, we might want to add a parameter to it to manually define the world
instead of relying on the world of the player. This also allows us to use the placeholder in an independent context.
For that, we will have to implement the NullablePlaceholder interface instead of OnlinePlaceholder.
public class WeatherPlaceholder implements NullablePlaceholder {
@Nullable
private final Argument<World> world;
public WeatherPlaceholder(@Nullable final Argument<World> world) {
this.world = world;
}
@Override
public String getValue(final Profile profile) throws QuestException {
return getWeather(world.getValue(profile)); //(1)!
}
}
- Call the
getWeathermethod with the resolved world value to get the weather value and return it.
Summaryπ
To create a placeholder, we need to implement one of the following interfaces:
OnlinePlaceholderto require the player the placeholder is being resolved for to be online.PlayerPlaceholderto require a player the placeholder is being resolved for.PlayerlessPlaceholderto not require a player and resolve the placeholder independently.NullablePlaceholderto allow the placeholder to be resolved with or without a player.
Using Arguments allows us to parameterize the placeholder and even use further placeholders as parameters.
How to create a factory for a placeholderπ
After creating a placeholder, we have to create a factory for it. Factories are used to create instances of a placeholder for a specific instruction.
In the following we are going to create factories for each of the WeatherPlaceholder placeholder approaches.
Approach #1: A simple factoryπ
See Approach #1.
The easiest way to create a factory is to extend the PlayerlessPlaceholderFactory or PlayerPlaceholderFactory class.
Those simple factories can be implemented inline during the registration of the placeholder. However, it is recommended
to create a separate class for each factory to keep readability and maintainability and avoid sneaky bugs.
In our case, we will create a WeatherPlaceholderFactory class implementing the PlayerlessPlaceholderFactory
class, since our placeholder is a PlayerlessPlaceholder.
public class WeatherPlaceholderFactory implements PlayerlessPlaceholderFactory {
@Override
public PlayerlessPlaceholder parsePlayerless(final Instruction instruction) throws QuestException {
return new WeatherPlaceholder();
}
}
YAML usage examples
actions:
logWeather: "log current weather in world is %weather%"
Approach #2: Using a different factoryπ
See Approach #2.
The factory for our second approach is almost the same as the first one.
Since we are using the OnlinePlaceholder interface for our placeholder, we will have to implement the
PlayerPlaceholderFactory class instead and wrap our instance in an OnlinePlaceholderAdapter.
It might be hard to see the difference in the usage examples. In approach #1 the placeholder is independent of the player and might therefore be used in an independent context. In our second approach, on the other hand, it is required to use the player, and therefore the action will fail if it is used in an independent context.
public class WeatherPlaceholderFactory implements PlayerPlaceholderFactory {
@Override
public PlayerlessPlaceholder parsePlayerless(final Instruction instruction) throws QuestException {
return new OnlinePlaceholderAdapter(new WeatherPlaceholder());
}
}
YAML usage examples
actions:
logWeather: "log current weather in world is %weather%"
Approach #3: Adding versatilityπ
See Approach #3.
The placeholder we created in the third approach now requires a parameter to be specified.
We also used the NullablePlaceholder interface to allow the placeholder to be resolved with or without a player.
Therefore, we will have to implement both the PlayerlessPlaceholderFactory and PlayerPlaceholderFactory
interfaces and wrap our instance in a NullablePlaceholderAdapter after parsing the world argument.
public class WeatherPlaceholderFactory implements PlayerlessPlaceholderFactory, PlayerPlaceholderFactory {
@Override
public PlayerlessPlaceholder parsePlayerless(final Instruction instruction) throws QuestException {
return parse(instruction);
}
@Override
public PlayerPlaceholder parsePlayer(final Instruction instruction) throws QuestException {
return parse(instruction);
}
private NullablePlaceholderAdapter parse(final Instruction instruction) throws QuestException {
final Argument<World> world = instruction.world().get("world").orElse(DefaultArguments.PLAYER_WORLD); //(1)!
return new NullablePlaceholderAdapter(new WeatherPlaceholder(world));
}
}
- The
worldargument of theweatherplaceholder is optional and defaults to the player's world.
YAML usage examples
actions:
logWeather: "log current weather in world is %weather%" #(1)!
logWeatherInWorld: "log current weather in world 'somewhere' is %weather.world:somewhere%" #(2)!
- The
worldargument of theweatherplaceholder is optional and defaults to the player's world. This will require the player to be online to resolve the placeholder. - The
worldargument of theweatherplaceholder is given and this may be used in an independent context.
Summaryπ
To create a factory for a placeholder, we need to implement one of the following interfaces:
PlayerPlaceholderFactoryto create a placeholder that requires a player.PlayerlessPlaceholderFactoryto create a placeholder that does not require a player.
For NullablePlaceholders, we need to implement both the PlayerlessPlaceholderFactory and PlayerPlaceholderFactory
interfaces and wrap our instance in a NullablePlaceholderAdapter.
Since OnlinePlaceholders require a player to be online, we need to implement the PlayerPlaceholderFactory interface
and wrap our instance in an OnlinePlaceholderAdapter.
How to register a placeholder with BetonQuestπ
After creating a placeholder and its factory, we have to register them with BetonQuest.
The name of the placeholder used in the script is now defined in the registration process.
Given the api is already obtained, we can access the placeholder registry and register the factory as follows:
public class MyPlugin extends JavaPlugin {
@Override
public void onEnable() {
// ...
betonQuestApi.placeholders().registry().register("weather", new WeatherPlaceholderFactory()); //(1)!
// ...
}
}
- Only viable for approaches #1 and #2.
Since a NullablePlaceholder requires both a PlayerlessPlaceholderFactory and a PlayerPlaceholderFactory, we
need to register them combined:
public class MyPlugin extends JavaPlugin {
@Override
public void onEnable() {
// ...
betonQuestApi.placeholders().registry().registerCombined("weather", new WeatherPlaceholderFactory()); //(1)!
// ...
}
}
- Only viable for approach #3
How to resolve a placeholderπ
In comparison to actions and conditions, there are multiple ways to resolve placeholders. We can't resolve existing ones defined in the script, but we can resolve strings representing placeholders.
The most reliable way to resolve a placeholder is to create an Argument for it using the PlaceholderManager class.
But it is also possible to use the getValue method of the PlaceholderManager class to directly resolve the
placeholder if there is no need to create an Argument instance for dynamic resolution.
The following examples contain an instruction representing the entire placeholder string including the % symbols:
%weather%%weather.world:somewhere%
public class MyFeature {
private final BetonQuestApi betonQuestApi;
public String resolve(@Nullable final QuestPack pack, final String instruction, @Nullable final Profile profile) {
final Argument<String> resolved = betonQuestApi.placeholders().manager().create(pack, instruction); //(1)!
return resolved.getValue(profile); //(2)!
}
}
- The
instructionis parsed to create anArgumentinstance. - The value of the
Argumentis resolved using theprofile.
public class MyFeature {
private final BetonQuestApi betonQuestApi;
public String resolve(final QuestPack pack, final String instruction, @Nullable final Profile profile) {
return betonQuestApi.placeholders().manager().getValue(pack, instruction, profile); //(1)!
}
}
- The
instructionis parsed using theprofileto resolve the placeholder.
How a full example looks likeπ
WeatherPlaceholder.java
public class WeatherPlaceholder implements NullablePlaceholder {
@Nullable
private final Argument<World> world;
public WeatherPlaceholder(@Nullable final Argument<World> world) {
this.world = world;
}
@Override
public String getValue(final Profile profile) throws QuestException {
return getWeather(world.getValue(profile)); //(1)!
}
private String getWeather(final World world) {
if(world.isThundering()) {
return "thunder";
}
if(world.hasStorm()) {
return "rain";
}
return "clear";
}
}
- Call the
getWeathermethod with the resolved world value to get the weather value and return it.
WeatherPlaceholderFactory.java
public class WeatherPlaceholderFactory implements PlayerlessPlaceholderFactory, PlayerPlaceholderFactory {
@Override
public PlayerlessPlaceholder parsePlayerless(Instruction instruction) throws QuestException {
return parse(instruction);
}
@Override
public PlayerPlaceholder parsePlayer(Instruction instruction) throws QuestException {
return parse(instruction);
}
private NullablePlaceholderAdapter parse(final Instruction instruction) throws QuestException {
final Argument<World> world = instruction.world().get("world").orElse(DefaultArguments.PLAYER_WORLD); //(1)!
return new NullablePlaceholderAdapter(new WeatherPlaceholder(world));
}
}
- The
worldargument of theweatherplaceholder is optional and defaults to the player's world.
YAML usage examples
actions:
logWeather: "log current weather in world is %weather%" #(1)!
logWeatherInWorld: "log current weather in world 'somewhere' is %weather.world:somewhere%" #(2)!
- The
worldargument of theweatherplaceholder is optional and defaults to the player's world. This will require the player to be online to resolve the placeholder. - The
worldargument of theweatherplaceholder is given and this may be used in an independent context.
What limitations are thereπ
Force synchronous placeholder resolutionπ
Generally, BetonQuest tries to resolve placeholders asynchronously if their parent actions, conditions, etc. are in an asynchronous context. In some contexts, especially if a placeholder interacts with the bukkit api, it may be necessary to resolve it synchronously by itself.
To mark a placeholder as required to resolve in sync with the main thread, overwrite the isPrimaryThreadEnforced()
method inherented from the PrimaryThreadEnforceable interface, which is implemented by all placeholders by default:
public class MySyncPlaceholder implements PlayerPlaceholder {
public MySyncPlaceholder() {
}
@Override
public String getValue(final Profile profile) {
// ...
}
@Override
public boolean isPrimaryThreadEnforced() {
return true; //(1)!
}
}
- This method returns
falseby default, so all placeholders are tested asynchronously if not defined otherwise.