Minecraft Modding: Advancements

Advancements


Prior to 1.12 there was a different system called Achievements. If you are modding older versions you can check out my tutorial on Achievements.

One of the key changes is that Advancements have become resource-based meaning that custom advancements can be added by simply applying a resource pack rather than any mod programming required. The advancements are defined in a JSON format which is explained well in this Advancement overview. You should also review the details about the existing advancements and JSON format.

Warning: If your advancement should be the root of a new tab, don't specify the parent element in the JSON file at all. If you specify the parent element, use an advancement that exists. If you specify the parent element and the specified advancement doesn't exist, AdvancementList#loadAdvancements will ignore your advancement.

Creating Custom Advancements


If your idea for a custom advancement can be accomplished by combining existing triggers and conditions, then all you have to do is add a JSON to your advancements assets folder that puts that together. Just follow the information in the links given in the Background section above to make sure you properly form the JSON and understand the available built-in triggers and conditions.

However, to do fancier things you may need either custom triggers, custom conditions or both. For example, I have a mod where I have entities that can be tamed but they extend EntityFlying which does not extend EntityTameable. Therefore, the "tame_animal" trigger cannot be used because it works only with EntityTameable. So I needed a custom trigger. 

Creating Custom Triggers (Criteria)


If necessary you may need to make a custom criterion. I explain one approach to doing custom criterion here.

Creating Custom Conditions

Although there are a lot of predefined triggers (see list here) you may feel the need to create a custom trigger. Thanks to Choonster for explaining how to do this, as follows.

To create a custom trigger you need to:
  1. Create a class that extends ItemPredicate
  2. @Override the ItemPredicate#test() method
  3. Register it with ItemPredicate.register() method.
You can then use this anywhere an item predicate is expected (e.g. in the inventory_changed trigger). Item predicates are used by advancement criteria like inventory_changed to determine whether or not the item matches the requirements. The inventory_changed criterion will be met when all of the specified item predicates match the item in any slot of the player's inventory (i.e. the player has every required item). For the inventory_changed criterion, each object in the items array is an item predicate.

If you set the type element of an item predicate to a string, Forge will use the custom ItemPredicate factory that was registered with that name (via ItemPredicate.register() method) instead of creating a vanilla ItemPredicate instance.

Deleting / Removing A Vanilla Advancement


I haven't tried this myself, but here is some suggestions from Choonster on how to proceed.

There doesn't seem to be an easy way to remove vanilla advancements without messing around with the internals of AdvancementManager. WorldServer#getAdvancementManager returns the server world's AdvancementManager. (The World#advancementManager field is never used by client worlds so it is not clear why it's in World rather than WorldServer.)
In particular, advancements are actually stored in the AdvancementList instance (AdvancementManager.ADVANCEMENT_LIST and ClientAdvancementManager#advancementList). You'll need to use Java reflection techniques to access the server instance and the internal collections.

Advancements are loaded when the server starts (WorldServer#init, called from MinecraftServer#loadAllWorlds) and when it's asked to reload them (MinecraftServer#reload). WorldEvent.Load will be fired for dimension 0 just after the initial load, but there's currently no event fired for the reload. 

This Forge pull request adds an event for the reload, but it's not likely to be merged until the author documents the event. If you look at the usages of MinecraftServer#reload, you'll see that it's called from CommandReload#execute (i.e. the /reload command) and from Minecraft#refreshResources (i.e. the method used to reload resource packs) only when there's an integrated server running (i.e. single player or the LAN host).

For WorldEvent.Load, use WorldEvent#getWorld to get the World. Most other places will have a World argument/field available. If World#isRemote is false, you're on the logical server and the World is a WorldServer.
If you really don't have a World or WorldServer available, use DimensionManager.getWorld to get the WorldServer for dimension 0.

No comments:

Post a Comment