Objectives
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.
Objective API Classes
org.betonquest.betonquest.api.service.objective.Objectivesorg.betonquest.betonquest.api.service.objective.ObjectiveManagerorg.betonquest.betonquest.api.service.objective.ObjectiveRegistryorg.betonquest.betonquest.api.quest.objective.Objectiveorg.betonquest.betonquest.api.quest.objective.ObjectiveFactory
Introductionπ
This page covers the Objective API of BetonQuest.
You should have viewed these pages
What this page covers
What this page does not cover
- What an objective is
- Which objectives are available
- How to work with objectives in scripting
How to create an objectiveπ
Creating an objective will be explained in the following sections using the InventoryObjective as an example.
Comparison: Helper classπ
Before we start creating our objective, we will take a look at the DefaultObjective class.
This class is a helper class in the library package that is used to create objectives and might offer some useful methods.
The current implementation of the DefaultObjective class just stores the required ObjectiveService in a field as required by Objective anyways.
Those examples are identical in their behavior, but in the examples below, only the DefaultObjective class is used:
public class SimpleObjective implements Objective {
private final ObjectiveService objectiveService;
public SimpleObjective(final ObjectiveService objectiveService) {
this.objectiveService = objectiveService;
}
@Override
public ObjectiveService getService() {
return objectiveService;
}
}
public class SimpleObjective extends DefaultObjective {
public SimpleObjective(final ObjectiveService objectiveService) {
super(objectiveService);
}
}
Approach 1: A simple objectiveπ
In the first approach, we will try to create a simple objective that will complete if the player opens an inventory.
- The
onOpenInventorywill be called when the player with a certain profile opens an inventory (as later defined in the factory). The name is freely chosen. - The
ObjectiveServiceis used for objective-related tasks inside of an objective. - The
completemethod is called when the objective is completed for a certain profile.
public class InventoryObjective extends DefaultObjective {
public InventoryObjective(final ObjectiveService objectiveService) {
super(objectiveService);
}
public void onOpenInventory(final InventoryOpenEvent event,
final OnlineProfile onlineProfile) throws QuestException {
getService().complete(onlineProfile);
}
}
And that's it for the objective itself! After registering the objective with its factory, the player will be able to complete the objective by opening any inventory.
Approach 2: Adding parametersπ
The logical next step is to add parameters to the objective and make it more configurable and therefore more useful. Let's add a parameter to the objective that will be used to allow only a specific type of inventory to be opened.
The Argument class is used to define arguments for an objective that can be resolved for a profile
and thereby resolve any contained placeholders for that profile.
public class InventoryObjective extends DefaultObjective {
private final Argument<InventoryType> inventoryType;
public InventoryObjective(final ObjectiveService objectiveService, final Argument<InventoryType> inventoryType) {
super(objectiveService);
this.inventoryType = inventoryType;
}
public void onOpenInventory(final InventoryOpenEvent event,
final OnlineProfile onlineProfile) throws QuestException {
if (event.getInventory().getType() != inventoryType.getValue(onlineProfile)) {
return;
}
getService().complete(onlineProfile);
}
}
Approach 3: Adding more eventsπ
Suppose we want to add more events to the objective. For example, we want the player to open an inventory of a certain type and then close it again. We can do this by adding a new event to the objective in a similar way as we did in the previous approaches, and the name is freely chosen again.
public class InventoryObjective extends DefaultObjective {
private final Argument<InventoryType> inventoryType;
private final Set<UUID> openInventories;
public InventoryObjective(final ObjectiveService objectiveService, final Argument<InventoryType> inventoryType) {
super(objectiveService);
this.inventoryType = inventoryType;
this.openInventories = new HashSet<>();
}
public void onOpenInventory(final InventoryOpenEvent event,
final OnlineProfile onlineProfile) throws QuestException {
if (event.getInventory().getType() != inventoryType.getValue(onlineProfile)) {
return;
}
openInventories.add(onlineProfile.getPlayerUUID());
}
public void onCloseInventory(final InventoryCloseEvent event,
final OnlineProfile onlineProfile) throws QuestException {
if (!openInventories.remove(onlineProfile.getPlayerUUID())) {
return;
}
getService().complete(onlineProfile);
}
}
Summaryπ
To create an objective, we need to create a class that implements the Objective interface.
Alternatively, we can extend the DefaultObjective class instead to reduce the amount of boilerplate code we need to
write.
To include events, we need to add methods that are called when the corresponding event is fired. Those methods contain the event instance itself and optionally the profile that is involved.
You may:
- Include no profile at all
- Include a
Profile - Include an
OnlineProfile
Using Arguments allows us to parameterize the objective and to allow placeholders in those parameters.
How to create a factory for an objectiveπ
After creating an objective, we have to create a factory for it. Factories are used to create instances of the objective for a specific instruction.
In the following we are going to create factories for each of the InventoryObjective objective approaches.
Approach #1: A simple factoryπ
See Approach #1.
To implement a factory, we need to create a class that implements the ObjectiveFactory interface.
In the factory, the provided instruction is parsed to create the objective instance
and register its events with the ObjectiveService.
public class InventoryObjectiveFactory implements ObjectiveFactory {
@Override
public Objective parseInstruction(final Instruction instruction,
final ObjectiveService service) throws QuestException {
final InventoryObjective objective = new InventoryObjective(service); //(1)!
service.request(InventoryOpenEvent.class) //(2)!
.priority(EventPriority.LOWEST) //(3)!
.onlineHandler(objective::onOpenInventory) //(4)!
.entity(InventoryOpenEvent::getPlayer) //(5)!
.subscribe(true); //(6)!
return objective; //(7)!
}
}
- Create an instance of the objective.
- Request the
InventoryOpenEventevent for the objective. - Set the event's priority as commonly done in listeners of the bukkit API.
- Set the event handler of the
InventoryOpenEventfor the objective. This expects a consumer of the objective; usually a method reference. - To parse the online profile automatically, we have to tell the service where to retrieve it from.
- Subscribe the event to the service. This concludes the registration of the current event request.
- Return the objective instance.
YAML usage examples
objectives:
trackInv: "inventory"
Approach #2: Parameterized factoryπ
See Approach #2.
Reading a parameter in addition to registering the event is done in the same way as in action, conditions and other factories.
public class InventoryObjectiveFactory implements ObjectiveFactory {
@Override
public Objective parseInstruction(final Instruction instruction,
final ObjectiveService service) throws QuestException {
final Argument<InventoryType> inventoryType = instruction.enumeration(InventoryType.class).get(); //(1)!
final InventoryObjective objective = new InventoryObjective(service, inventoryType); //(2)!
service.request(InventoryOpenEvent.class) //(3)!
.priority(EventPriority.LOWEST) //(4)!
.onlineHandler(objective::onOpenInventory) //(5)!
.entity(InventoryOpenEvent::getPlayer) //(6)!
.subscribe(true); //(7)!
return objective; //(8)!
}
}
- Parse the first parameter of the instruction as an
InventoryTypeargument. - Create an instance of the objective.
- Request the
InventoryOpenEventevent for the objective. - Set the event's priority as commonly done in listeners of the bukkit API.
- Set the event handler of the
InventoryOpenEventfor the objective. This expects a consumer of the objective; usually a method reference. - To parse the online profile automatically, we have to tell the service where to retrieve it from.
- Subscribe the event to the service. This concludes the registration of the current event request.
- Return the objective instance.
YAML usage examples
objectives:
trackInv: "inventory CHEST"
Approach #3: Multiple eventsπ
See Approach #3.
To register multiple events, we can simply add more event requests to the service.
public class InventoryObjectiveFactory implements ObjectiveFactory {
@Override
public Objective parseInstruction(final Instruction instruction,
final ObjectiveService service) throws QuestException {
final Argument<InventoryType> inventoryType = instruction.enumeration(InventoryType.class).get();
final InventoryObjective objective = new InventoryObjective(service, inventoryType);
service.request(InventoryOpenEvent.class)
.priority(EventPriority.LOWEST)
.onlineHandler(objective::onOpenInventory)
.entity(InventoryOpenEvent::getPlayer)
.subscribe(true);
service.request(InventoryCloseEvent.class) //(1)!
.onlineHandler(objective::onCloseInventory) //(2)!
.entity(InventoryCloseEvent::getPlayer) //(2)!
.subscribe(true);
return objective;
}
}
- Make sure to request the
InventoryCloseEventevent for the objective the same way as above. - Make sure to reference the correct method!
YAML usage examples
objectives:
trackInv: "inventory CHEST"
Summaryπ
To create a factory for an objective, we need to create a class that implements the ObjectiveFactory interface.
In the factory, you may parse the instruction to obtain arguments to create a parameterized objective instance.
Register its events with the ObjectiveService.
Details about the ObjectiveServiceπ
Since the ObjectiveService is central to interact with BetonQuest, we will go through its methods in more detail.
Some methods are only useful in the context of the factory for the objective, some are only useful in the context of the objective itself.
Factory methods
Methods that are only useful in the context of the factory for the objective.
| Method | Description |
|---|---|
request(Class<Event>) |
Requests an event of the given type. |
Objective methods
Methods that are only useful in the context of the objective itself.
| Method | Description |
|---|---|
getProperties() |
Access the properties of the objective. |
complete(Profile) |
Completes the objective for the given profile. |
getExceptionHandler() |
Retrieves the exception handler for the objective. |
containsProfile(Profile) |
Checks if the given profile is currently involved in the objective. |
callActions(Profile) |
Calls the objective's actions for the given profile. |
checkConditions(Profile) |
Checks if the objective's conditions are met for the given profile. |
General methods
Methods that can be useful in both contexts.
| Method | Description |
|---|---|
getServiceDataProvider() |
Access the service data provider of the objective. |
getObjectiveID() |
Retrieves the objective ID of the objective. |
getProfileProvider() |
Access the profile provider of BetonQuest. |
getLogger() |
Retrieves the logger for the objective. |
Event subscription with the request builderπ
The request(Class<Event>) method of the ObjectiveService returns a builder that allows us to configure the event request.
Each request must fulfill certain requirements to be registered successfully.
Event subscription methods
| Method | Description | Info |
|---|---|---|
handler(NonProfileHandler) |
Define how the event is handled. | |
handler(ProfileHandler) |
Define how the event is handled. | |
onlinehandler(Handler) |
Define how the event is handled. | |
uuid(Extractor) |
Extract a players UUID from the event. |
|
offlinePlayer(Extractor) |
Extract an OfflinePlayer from the event. |
|
player(Extractor) |
Extract a Player from the event. |
|
entity(Extractor) |
Extract an Entity from the event that may be a Player. |
|
profile(Extractor) |
Extract a Profile from the event. |
|
priority(EventPriority) |
Sets the priority for the event request. | def: NORMAL |
ignoreConditions() |
Ignores conditions set for the objective for this event. | def: unset |
subscribe(boolean) |
Finalize event subscription and define ignoreCancelled |
The following examples show how to subscribe to an event with the request builder:
Minimal example: VehicleMoveEvent
public void onVehicleMove(final VehicleMoveEvent event) throws QuestException {
// ...
}
service.request(VehicleMoveEvent.class) //(1)!
.handler(objective::onVehicleMove) //(2)!
.subscribe(true); //(3)!
- Request the
VehicleMoveEventevent. - Define the event handler for the objective that does not require a profile.
- Finalize the event subscription and set
ignoreCancelledtotrueskipping cancelled events.
Minimal example: PlayerJumpEvent
public void onPlayerJump(final PlayerJumpEvent event,
final OnlineProfile onlineProfile) throws QuestException {
// ...
}
service.request(PlayerJumpEvent.class) //(1)!
.onlineHandler(objective::onPlayerJump) //(2)!
.player(PlayerJumpEvent::getPlayer) //(3)!
.subscribe(true); //(4)!
- Request the
PlayerJumpEventevent. - Define the event handler for the objective that does require a profile.
- Extract the player from the event that the profile should be parsed from.
- Finalize the event subscription and set
ignoreCancelledtotrueskipping cancelled events.
Minimal example: PlayerConversationStartEvent
public void onConversationStart(final PlayerConversationStartEvent event,
final Profile profile) throws QuestException {
// ...
}
service.request(PlayerConversationStartEvent.class) //(1)!
.handler(objective::onConversationStart) //(2)!
.profile(PlayerConversationStartEvent::getProfile) //(3)!
.subscribe(true); //(4)!
- Request the
PlayerConversationStartEventevent. - Define the event handler for the objective that does require a profile.
- Extract the profile from the event directly.
- Finalize the event subscription and set
ignoreCancelledtotrueskipping cancelled events.
Objective propertiesπ
To provide access to the objective's properties via an objective placeholder, we can define properties in the objective's constructor.
public class InventoryObjective extends DefaultObjective {
private final Argument<InventoryType> inventoryType;
private final Set<UUID> openInventories;
public InventoryObjective(final ObjectiveService objectiveService, final Argument<InventoryType> inventoryType) {
super(objectiveService);
this.inventoryType = inventoryType;
this.openInventories = new HashSet<>();
final QuestFunction<Profile, String> property = profile -> inventoryType.getValue(profile).name(); //(1)!
service.getProperties().setProperty("type", property); //(2)!
}
// ...
}
- Define a property for the inventory type by resolving the inventory type argument for the profile.
- Set the
typeproperty of the objective.
How to register an objective and its factory with BetonQuestπ
After creating an objective and its factory, we can register them with BetonQuest. The name of the objective used in the script is now defined in the registration process. Given the api is already obtained, we can access the objective registry and register the factory as follows:
public class MyPlugin extends JavaPlugin {
@Override
public void onEnable() {
// ...
betonQuestApi.objectives().registry().register("inventory", new InventoryObjectiveFactory()); //(1)!
// ...
}
}
- Register the factory for
inventoryto the objective registry.
How to access an objectiveπ
To access objectives, we can use the ObjectiveManager in the api.
The following examples are put together even though they are not related to each other.
public class MyFeature {
private final BetonQuestApi betonQuestApi;
public void foo(final Profile profile, final ObjectiveIdentifier identifier) throws QuestException {
final ObjectiveManager objectiveManager = betonQuestApi.objectives().manager();
final List<Objective> objectives = objectiveManager.getForProfile(profile); //(1)!
final Objective objective = objectiveManager.getObjective(identifier); //(2)!
objectiveManager.start(profile, identifier); //(3)!
objectiveManager.cancel(profile, identifier); //(4)!
}
}
- Obtain all objectives for the given profile.
- Obtain the objective with the given identifier.
- Start the objective for the given profile.
- Cancel the objective for the given profile.
How a full example looks likeπ
InventoryObjective.java
public class InventoryObjective extends DefaultObjective {
private final Argument<InventoryType> inventoryType;
private final Set<UUID> openInventories;
public InventoryObjective(final ObjectiveService objectiveService, final Argument<InventoryType> inventoryType) {
super(objectiveService);
this.inventoryType = inventoryType;
this.openInventories = new HashSet<>();
}
public void onOpenInventory(final InventoryOpenEvent event,
final OnlineProfile onlineProfile) throws QuestException {
if (event.getInventory().getType() != inventoryType.getValue(onlineProfile)) {
return;
}
openInventories.add(onlineProfile.getPlayerUUID());
}
public void onCloseInventory(final InventoryCloseEvent event,
final OnlineProfile onlineProfile) throws QuestException {
if (!openInventories.remove(onlineProfile.getPlayerUUID())) {
return;
}
getService().complete(onlineProfile);
}
}
InventoryObjectiveFactory.java
public class InventoryObjectiveFactory implements ObjectiveFactory {
@Override
public Objective parseInstruction(final Instruction instruction,
final ObjectiveService service) throws QuestException {
final Argument<InventoryType> inventoryType = instruction.enumeration(InventoryType.class).get();
final InventoryObjective objective = new InventoryObjective(service, inventoryType);
service.request(InventoryOpenEvent.class)
.priority(EventPriority.LOWEST)
.onlineHandler(objective::onOpenInventory)
.entity(InventoryOpenEvent::getPlayer)
.subscribe(true);
service.request(InventoryCloseEvent.class) //(1)!
.onlineHandler(objective::onCloseInventory) //(2)!
.entity(InventoryCloseEvent::getPlayer) //(2)!
.subscribe(true);
return objective;
}
}
YAML usage examples
objectives:
trackInv: "inventory CHEST"
What limitations are thereπ
Synchronous objective completionπ
All objectives retain their context by default. But since most bukkit events are mostly synchronous, objectives usually are too.
Legacy counting objectiveπ
There is a legacy org.betonquest.betonquest.api.CountObjective that is not recommended to use.
It is internally used to measure numerical progress on objectives.
It is explicitly not intended to be used by integrations as it is not part of the official API and scheduled for removal.