Building BeardAchievements

@nnotate all the things!

This is part of a series of posts regarding BeardAch, a Bukkit plugin I’ve worked on for a little over 3 years.
BeardAch has a number of interesting systems that I want to write about. How they work, what they allow us to achieve.

There are many ways to configure an application. Putting aside the innumerable amount of file configuration formats (a topic for another post), there are plenty of ways to load in or initialize the various components of an application.

BeardAch consists of three core primitives:

  • Achievement – Pretty much just a configuration container, holding things such as it’s id, name, description, and configuration details for a list of triggers and rewards.
  • ITrigger –  An interface for a predicate that takes a player and configuration alongside data from Bukkit and other plugins.
  • IReward – An interface for a action performed when an Achievement’s collection of triggers return true. It also takes a player and a configuration.

Both ITrigger and IReward share a common base of IConfigurable to provide deserialization. Initially, Achievements were loaded from YAML. It worked, but it was horrendous to create an achievement with, and a lot of code and time was taken up with parsing the config to load achievements. Eventually I moved the achievement data to JSON using Google’s GSON library.

I use annotations to make development easier. Annotations are a mechanism built into java that allows you to associate data with classes, methods, fields and parameters. Often you come across @override and @deprecated.

In BeardAch we use custom annotations to:

  • Add an identifier to the various implementations of ITrigger and IReward, so that we can deserialize correctly.
  • Add metadata about an implementation (human readable name, plugin dependencies for conditional loading)
  • Field type data for external editor tooling (labels, min/max values, enums to use).

At compile time we use an annotation processor to read @Configurable tags and generate a manifest file, this saves us having to scan the whole classpath at runtime and instead load this list. This approach also allows modification of what is loaded after compilation, and makes it trivial to add support for loading extensions containing additional triggers and rewards.

At runtime, annotations on a trigger or reward related field types or metadata are parsed, allowing us to generate a configuration for the editor when it is exported. This allows us to provide editor support to custom triggers and rewards easily.

Next time, we shall look at the editor, and how it turns this information into a usable form for creating achievements.