Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update documentation on custom Advancement criteria #45

Merged
merged 3 commits into from
Jan 16, 2024
Merged
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
101 changes: 51 additions & 50 deletions docs/resources/server/advancements.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
Advancements
============
# Advancements

Advancements are tasks that can be achieved by the player which may advance the progress of the game. Advancements can trigger based on any action the player may be directly involved in.

All advancement implementations within vanilla are data driven via JSON. This means that a mod is not necessary to create a new advancement, only a [data pack][datapack]. A full list on how to create and put these advancements within the mod's `resources` can be found on the [Minecraft Wiki][wiki]. Additionally, advancements can be [loaded conditionally and defaulted][conditional] depending on what information is present (mod loaded, item exists, etc.).
All advancement implementations within vanilla are data driven via JSON. This means that a mod is not necessary to create a new advancement, only a [data pack][datapack]. A full list on how to create and put these advancements within the mod's `resources` can be found on the [Minecraft Wiki][wiki]. Additionally, advancements can be [loaded conditionally and defaulted][conditional] depending on what information is present (mod loaded, item exists, etc.). As with other data driven features, advancements can be generated via [data generators][datagen].

Advancement Criteria
--------------------
## Advancement Criteria

To unlock an advancement, the specified criteria must be met. Criteria are tracked through triggers which execute when a certain action is performed: killing an entity, changing an inventory, breading animals, etc. Any time an advancement is loaded into the game, the criteria defined are read and added as listeners to the trigger. Afterwards a trigger function is called (usually named `#trigger`) which checks all listeners as to whether the current state meets the conditions of the advancement criteria. The criteria listeners for the advancement are only removed once the advancement has been obtained by completing all requirements.

Expand Down Expand Up @@ -43,43 +41,31 @@ A list of criteria triggers defined by vanilla can be found in `CriteriaTriggers

### Custom Criteria Triggers

Custom criteria triggers can be created by implementing `SimpleCriterionTrigger` for the created `AbstractCriterionTriggerInstance` subclass.
Custom criteria triggers are made up of two parts: the trigger, which is activated in code at some point you specify by calling `#trigger`, and the instance which defines the conditions under which the trigger should award the criterion. The trigger extends `SimpleCriterionTrigger<T>` while the instance implements `SimpleCriterionTrigger.SimpleInstance`. The generic value `T` represents the trigger instance type.

### AbstractCriterionTriggerInstance Subclass
### The SimpleCriterionTrigger.SimpleInstance Implementation

The `AbstractCriterionTriggerInstance` represents a single criteria defined in the `criteria` object. Trigger instances are responsible for holding the defined conditions, returning whether the inputs match the condition, and writing the instance to JSON for data generation.
The `SimpleCriterionTrigger.SimpleInstance` represents a single criteria defined in the `criteria` object. Trigger instances are responsible for holding the defined conditions, and returning whether the inputs match the condition.

Conditions are usually passed in through the constructor. The `AbstractCriterionTriggerInstance` super constructor requires the instance to define the registry name of the trigger and the conditions the player must meet as an `ContextAwarePredicate`. The registry name of the trigger should be supplied to the super directly while the conditions of the player should be a constructor parameter.
Conditions are usually passed in through the constructor. The `SimpleCriterionTrigger.SimpleInstance` interface requires only one function, called `#player`, which returns the conditions the player must meet as an `Optional<ContextAwarePredicate>`. If the subclass is a Java record with a `player` parameter of this type (as below), the automatically generated `#player` method will suffice.

```java
// Where ID is the registry name of the trigger
public ExampleTriggerInstance(ContextAwarePredicate player, ItemPredicate item) {
super(ID, player);
// Store the item condition that must be met
public record ExampleTriggerInstance(Optional<ContextAwarePredicate> player, ItemPredicate item) implements SimpleCriterionTrigger.SimpleInstance {
// extra methods here
}
```

:::note
Typically, trigger instances have a static constructor which allow these instances to be easily created for data generation. These static factory methods can also be statically imported instead of the class itself.
Typically, trigger instances have static helper methods which construct the full `Criterion<T>` object from the arguments to the instance. This allows these instances to be easily created during data generation, but are optional.
legobmw99 marked this conversation as resolved.
Show resolved Hide resolved

```java
public static ExampleTriggerInstance instance(ContextAwarePredicate player, ItemPredicate item) {
return new ExampleTriggerInstance(player, item);
// In this example, EXAMPLE_TRIGGER is a DeferredHolder<CriterionTrigger<?>>
public static Criterion<ExampleTriggerInstance> instance(ContextAwarePredicate player, ItemPredicate item) {
return EXAMPLE_TRIGGER.get().createCriterion(new ExampleTriggerInstance(Optional.of(player), item));
legobmw99 marked this conversation as resolved.
Show resolved Hide resolved
}
```
:::

Additionally, the `#serializeToJson` method should be overridden. The method should add the conditions of the instance to the other JSON data.

```java
@Override
public JsonObject serializeToJson(SerializationContext context) {
JsonObject obj = super.serializeToJson(context);
// Write conditions to json
return obj;
}
```

Finally, a method should be added which takes in the current data state and returns whether the user has met the necessary conditions. The conditions of the player are already checked through `SimpleCriterionTrigger#trigger(ServerPlayer, Predicate)`. Most trigger instances call this method `#matches`.

```java
Expand All @@ -92,53 +78,64 @@ public boolean matches(ItemStack stack) {

### SimpleCriterionTrigger

The `SimpleCriterionTrigger<T>` subclass, where `T` is the type of the trigger instance, is responsible for specifying the registry name of the trigger, creating a trigger instance, and a method to check trigger instances and run attached listeners on success.
The `SimpleCriterionTrigger<T>` subclass is responsible for specifying a codec to [serialize] the trigger instance `T` and supplying a method to check trigger instances and run attached listeners on success.

The registry name of the trigger is supplied to `#getId`. This should match the registry name supplied to the trigger instance.

A trigger instance is created via `#createInstance`. This method reads a criteria from JSON.
The latter is done by defining a method to check all trigger instances and run the listeners if their condition is met. This method takes in the `ServerPlayer` and whatever other data defined by the matching method in the `SimpleCriterionTrigger.SimpleInstance` subclass. This method should internally call `SimpleCriterionTrigger#trigger` to properly handle checking all listeners. Most trigger instances call this method `#trigger`.

```java
@Override
public ExampleTriggerInstance createInstance(JsonObject json, ContextAwarePredicate player, DeserializationContext context) {
// Read conditions from JSON: item
return new ExampleTriggerInstance(player, item);
// This method is unique for each trigger and is as such not a method to override
public void trigger(ServerPlayer player, ItemStack stack) {
this.trigger(player,
// The condition checker method within the SimpleCriterionTrigger.SimpleInstance subclass
triggerInstance -> triggerInstance.matches(stack)
);
}
```

Finally, a method is defined to check all trigger instances and run the listeners if their condition is met. This method takes in the `ServerPlayer` and whatever other data defined by the matching method in the `AbstractCriterionTriggerInstance` subclass. This method should internally call `SimpleCriterionTrigger#trigger` to properly handle checking all listeners. Most trigger instances call this method `#trigger`.
Finally, instances must be registered on the `Registries.TRIGGER_TYPE` registry. Techniques for doing so can be found under [Registries][registration].

### Serialization

A [codec] must be defined to serialize and deserialize the trigger instance. Vanilla typically creates this codec as a constant within the instance implementation that is then returned by the trigger's `#codec` method.


```java
// This method is unique for each trigger and is as such not overridden
public void trigger(ServerPlayer player, ItemStack stack) {
this.trigger(player,
// The condition checker method within the AbstractCriterionTriggerInstance subclass
triggerInstance -> triggerInstance.matches(stack)
);
class ExampleTrigger extends SimpleCriterionTrigger<ExampleTrigger.ExampleTriggerInstance> {
@Override
public Codec<ExampleTriggerInstance> codec() {
return ExampleTriggerInstance.CODEC;
}
// ...
public class ExampleTriggerInstance implements SimpleCriterionTrigger.SimpleInstance {
public static final Codec<ExampleTriggerInstance> CODEC = ...;
// ...
}
}
```

Afterwards, an instance should be registered using `CriteriaTriggers#register` during `FMLCommonSetupEvent`.
For the earlier example of a record with a `ContextAwarePredicate` and an `ItemPredicate`, the codec could be:

:::danger
`CriteriaTriggers#register` must be enqueued to the synchronous work queue via `FMLCommonSetupEvent#enqueueWork` as the method is not thread-safe.
:::
```java
RecordCodecBuilder.create(instance -> instance.group(
ExtraCodecs.strictOptionalField(EntityPredicate.ADVANCEMENT_CODEC, "player").forGetter(ExampleTriggerInstance::player),
ItemPredicate.CODEC.fieldOf("item").forGetter(ExampleTriggerInstance::item)
).apply(instance, ExampleTriggerInstance::new));
``````

### Calling the Trigger

Whenever the action being checked is performed, the `#trigger` method defined by the `SimpleCriterionTrigger` subclass should be called.

```java
// In some piece of code where the action is being performed
// Where EXAMPLE_CRITERIA_TRIGGER is the custom criteria trigger
// Again, EXAMPLE_TRIGGER is a supplier for the registered instance of the custom criteria trigger
public void performExampleAction(ServerPlayer player, ItemStack stack) {
// Run code to perform action
EXAMPLE_CRITERIA_TRIGGER.trigger(player, stack);
EXAMPLE_TRIGGER.get().trigger(player, stack);
}
```

Advancement Rewards
-------------------
## Advancement Rewards

When an advancement is completed, rewards may be given out. These can be a combination of experience points, loot tables, recipes for the recipe book, or a [function] executed as a creative player.

Expand All @@ -165,3 +162,7 @@ When an advancement is completed, rewards may be given out. These can be a combi
[conditional]: ./conditional.md#implementations
[function]: https://minecraft.wiki/w/Function_(Java_Edition)
[triggers]: https://minecraft.wiki/w/Advancement/JSON_format#List_of_triggers
[datagen]: ../../datagen/server/advancements.md#advancement-generation
[codec]: ../../datastorage/codecs.md
[registration]: ../../concepts/registries.md#methods-for-registering
[serialize]: #serialization