Triggevent is a comprehensive addon for FFXIV that provides cooldown and multi-target DoT tracking, easy triggers, a titan jail plugin, and more.
Groovy is a programming language which is interoperable with Java. This makes it perfect for exposing a scripting interface for Triggevent.
There are some examples on the Wiki.
Here’s a crash course on how the integration works.
First, head over to the Groovy tab. The default script is ‘Scratch’, which is not saved (except when you “Save As”).
You can play around here. For example, to see what global variables are currently available, delete the existing text,
and write globals.variables
. Then, click Execute (or press Ctrl-Enter). Below, you will see a list of all available
global variables.
Triggevent internally uses PicoContainer for its dependency injection. All of these are also inserted into the Groovy
global variables, as you see from running that command. The variable name will be the name of the class, but with the
first letter lowercase. Some of these also have shortcuts, like how xivStateImpl
is also available under the name
state
.
I won’t get into all the details, but Groovy has a lot of syntactical sugar compared to Java. For example, if you have
a method thing.getFoo()
(or thing.isFoo()
), you can simply write thing.foo
instead. Likewise, thing.setFoo(value)
can be simplified to thing.foo = value
.
Groovy Strings are another neat feature. Single-quoted strings are treated as literal, but double-quoted and triple-double
quoted strings allow interpretation of values within a ${ }
block.
Now, fire up the game if you haven’t already, and run another script: state.player
. Now, you’ll see some information
about your player down below, including all the properties of the object representing your player character. You can
keep drilling down. For example, state.player.job.healer
. It should return true
or false
depending on whether you are currently playing a healer job.
Now, let’s make a simple conditional, along with some Groovy strings:
if (state.player.job.healer) {
eventMaster.pushEvent(new TtsRequest("${state.player.job.friendlyName} is a healer"))
}
else {
eventMaster.pushEvent(new TtsRequest("${state.player.job.friendlyName} is a not healer"))
}
Your ACT should play a TTS message: either “(your current job) is a healer”, or “(your current job) is not a healer”.
You can now do full-blown triggers from within a Groovy script.
Important Note: The script will do nothing until it is run. After pasting the script, you can run it and it will work until you close the program. To make it persistent, save the script and check the “run on startup” box.
Here is an example of a simple (non-sequential) trigger:
groovyTriggers.add {
named "foo"
type AbilityCastStart
when { it.abilityIdMatches(0x5EF8) }
then { event, context ->
context.accept(new TtsRequest(event.ability.name))
log.info "bar"
}
}
Here’s a more in-depth trigger. This triggers when you start casting Broil IV. It will immediately call out TTS “foo”, and if you have the callout overlay enabled, will display a rapidly-changing number showing you how many milliseconds are left on the cast bar, with the text having a cyan color.
Then, once the cast snapshots, it will do a text-only callout showing the amount of damage the ability did, in magenta, with an icon taken from a status effect, replacing the previous text entirely (rather than displaying both), and will remain there for 1000 milliseconds (1 second).
// Don't forget the 'def' keyword
def abilityId = 0x6509
groovyTriggers.add {
// Name should be unique
named "Sequential Trigger Test"
// Allow multiple concurrent invocations
concurrency concurrent
// Start on casting Broil IV
when { AbilityCastStart acs -> acs.abilityIdMatches(abilityId) }
sequence { e1, s ->
// Call out "foo" and a constantly updating text display defined as a GroovyString,
// plus a color in long form
callout { tts "foo" text "${->e1.estimatedRemainingDuration.toMillis()}" color new Color(0, 255, 255) }
// Then wait for the cast to snapshot
snapshot = s.waitEvent(AbilityUsedEvent, aue -> aue.abilityIdMatches(abilityId))
// Do another callout, no TTS, just text. Color in short form, plus a status icon, and a specific duration
callout { text "${snapshot.damage}" color 255,0,255 statusIcon 0x77F replaces last duration 1000 }
}
}
Within triggers, you have some neat tools that you can use to build callouts. You can simply write callout "foo"
to make
a simple text and TTS callout. However, you can also do this in long form if you need more control:
// Examples
// Different text vs TTS
callout {
tts "Foo"
text "Bar"
// Use the icon from ability 0x89 (WHM Regen)
abilityIcon 0x89
}
callout {
// 'both' can be used when you want tts and text to be the same
both "Stuff"
}
callout {
tts "Foo"
// Text that will continuously update itself - you need the '->' to make this happen. Otherwise, it will snapshot the value when the callout fires.
text "${->e1.estimatedRemainingDuration}"
// RGB color - you can optionally specify alpha as the last argument
color 255,0,255
// Use the status effect's icon for the callout
statusIcon e1.buff.id
}
callout {
// If you wish to declare a local variable, you must use the 'def' keyword
def calloutText = "Foo"
// You can use the variable directly
tts calloutText
// Or, you can embed it in a string
text "${calloutText} ${->e1.estimatedRemainingDuration}"
}
callout {
// You can also declare your tts first, and then reference it in the on-screen text:
tts "Foo"
text "${tts} ${->e1.estimatedRemainingDuration}"
}
By default, a callout will last 5 seconds and then it will be removed. However, there are numerous alternatives:
// Change the duration directly
sequence { e1, s ->
callout {
both "Foo"
// 3 seconds
duration 3000
}
}
// Provide a condition. The callout will be displayed as long as the condition holds true.
// Note that if you want to use globals, you must first copy them to a local variable at the top level
// of your script. In this case, you would simply write `def buffs = buffs`.
// Note that this completely overrides the "duration" directive.
sequence { e1, s ->
callout {
tts "Foo"
text "${tts} ${->e1.estimatedRemainingDuration}"
displayWhile {->buffs.originalStatusActive(e1)}
}
}
// Specify that one callout should directly replace another callout. This can be useful for sequences of mechanics.
// All this entails is saving the first callout to a local variable, and then referencing it in the second.
sequence { e1, s ->
def first = callout {
tts "Foo"
text "${tts} ${->e1.estimatedRemainingDuration}"
}
waitMs 2000
callout {
tts "Bar"
text "${tts} ${->e1.estimatedRemainingDuration}"
replaces first
}
}
// 'last' is a special keyword that specifies that it should replace the last callout generated by this trigger
// This behaves identically to the previous example.
sequence { e1, s ->
callout {
tts "Foo"
text "${tts} ${->e1.estimatedRemainingDuration}"
}
waitMs 2000
callout {
tts "Bar"
text "${tts} ${->e1.estimatedRemainingDuration}"
replaces last
}
}
// Finally, you can use the 'forceExpire()' method to remove a callout from the screen
sequence { e1, s ->
def theCall = callout {
tts "Foo"
text "${tts} ${->e1.estimatedRemainingDuration}"
}
waitMs 2000
theCall.forceExpire()
}
By default, the ‘block’ concurrency mode is used. You can change it by adding
concurrency block
, concurrency replace
, or concurrency concurrent
.
You can write to the global namespace by using globals.varName = value
. This allows you to do things like
Custom Search Filters or
Hybrid Easy/Scripted Triggers.