Tutorial: Creating User-defined Milestones

Download Download EHFS Installer
Current version: 2.0.10
Starting with EHFS 1.13.0 users may create their own milestones in addition to the built-in milestones. This enables users to track anything they want, starting with complex boss mechanics to even raid members slacking. This tutorial provides a quick-start guide to writing your own milestones.

(Last update: 2011-07-08, EHFS 2.0.10)

Prerequisites

Writing your own milestones requires some basic programming skills. User-defined milestones are written in the Groovy scripting language. You use this language to hook into the EHFS API to react to fight events and generate milestone information. Understanding object-oriented programming basics and knowledge of Java and Java APIs are a big help.

Creating Your First Milestone

Scripted milestones are created using the EHFS GUI. To do that, choose Settings > Milestones from the main menu, then click the "Add" button. This will open the following dialog:

The fields should be pretty self-explanatory. The color is used to mark your milestones on the graphs.

Let's create a simple milestone now. Suppose you're playing a druid, and you want to show when Savage Defense procs. Savage Defense actually is a buff, and that buff's spell ID is 62606. So what we need to track is a player that is gaining a buff (or more generally an "aura") with that spell ID. The time between gaining the aura and the fading of it should be marked on the graphs.

This is the code you would need to enter into the "Groovy code" field:

class SavageDefenseMilestone extends AbstractUnitAuraMilestone {
	SavageDefenseMilestone() {
		super(
			player(EventUnit.DESTINATION),
			[62606] as int[])
	}

	void unitAuraApplied(String unitGuid, int startSeconds, int endSeconds) {
		def annotation = BoxAnnotation.createForUnit(
			startSeconds, endSeconds, unitGuid)
		addAnnotation("Savage Defense", annotation)
	}
}

Now try parsing a fight where a druid is tanking, and their unit's graph should look similar to this:

This is really an easy way to track single auras and mark them on graphs. Let's examine the code in more detail to see what it does.

class SavageDefenseMilestone extends AbstractUnitAuraMilestone {

This line declares a new Groovy class. You can see that our new class extends the already-existing class AbstractUnitAuraMilestone. In general, all milestone classes must implement the interface IMilestone. There are a number of ready-made implementations of this interface to ease the task of creating new milestones. AbstractUnitAuraMilestone is one of these classes that aids in tracking auras.

SavageDefenseMilestone() {

This is the constructor of our new class. It must have the same name as the class, have no return type, and must not take any arguments.

super(
	player(EventUnit.DESTINATION),
	[62606] as int[])

These lines do multiple things at once:

  • The super() call will invoke the parent class constructor, that is, the constructor of AbstractUnitAuraMilestone. Note that the constructor of AbstractUnitAuraMilestone takes two arguments: An AbstractUnitFilter and a variable array of spell IDs.
  • The call to player() creates an AbstractUnitFilter that will only match fight events where the destination (hence EventUnit.DESTINATION in the code) is a player.
  • The part "[62606] as int[]" creates an array of integers containing a single element, the spell ID 62606.

To summarize, this line invokes the constructor of the parent class, passing a filter that matches when the destination is a player only, and the desired spell ID.

void unitAuraApplied(String unitGuid, int startSeconds, int endSeconds) {

This is a method that you are required to implement in subclasses of AbstractUnitAuraMilestone. It is automatically invoked by the parent class whenever there is an event encountered in the log file that matches the player filter and the specified spell ID. It tells our milestone that an aura has been applied.

def annotation = BoxAnnotation.createForUnit(
	startSeconds, endSeconds, unitGuid)

These lines will create a box annotation (as opposed to a vertical line.) In this case, createForUnit() is used to create the annotation for a specific unit only, meaning it will only be visible on that unit's graph. (The annotation object is stored in the variable "annotation" to keep the code simple.)

addAnnotation("Savage Defense", annotation)

The last line adds the newly-created annotation to the graph, specifying a legend title as well.

Using Classes

In Groovy code, you may use any Java class available to the program. This is usually done by placing "import" statements before the Groovy class, for example:

import java.util.regex.Pattern

class MyClass {
	// ...
}

In EHFS, when parsing the Groovy code for your milestone, there are several import statements being added automatically. This means there is no need to add import statements for often-used classes inside EHFS.

The import statements automatically added by EHFS are (in addition to Groovy's own):

import de.eventhorizongilde.ehfs.chart.*
import de.eventhorizongilde.ehfs.event.*
import de.eventhorizongilde.ehfs.gamemechanic.*
import de.eventhorizongilde.ehfs.milestone.*
import de.eventhorizongilde.ehfs.milestone.filter.*
import static de.eventhorizongilde.ehfs.milestone.filter.Filters.*
import de.eventhorizongilde.ehfs.parse.*

Note that all static methods inside the "Filters" class are also imported. So instead of writing

def filter = Filters.player(EventUnit.DESTINATION)
you may simply write
def filter = player(EventUnit.DESTINATION)

This becomes quite handy when combining multiple filters using Filters.and() or Filters.or().

Event Filters

While it is possible to create milestones by directly implementing the IMilestone interface, this is generally not advisable. Milestone classes are usually interested in specific events only. To help filtering for events, there is an implementation of IMilestone available that does this: AbstractFilterMilestone.

When using AbstractFilterMilestone, you should use addFilter() to add one or more filters. You also need to implement the method "eventHappened(String key, CombatEvent event)" to get informed when an event matched a specific filter (indicated by the key.)

The AbstractUnitAuraMilestone class presented earlier is a specialized subclass of AbstractFilterMilestone.

See the Filters class for readily available filters, or implement the IEventFilter interface to create your own filters.

References



Get Event Horizon Fight Statistics at SourceForge.net. Fast, secure and Free Open Source software downloads