-
Notifications
You must be signed in to change notification settings - Fork 32
Why is Regex Bad for Triggers?
Regex is a very powerful text handling too in general. However, when it comes to FFXIV triggers specifically, Regex is not and never was the right tool for the job.
Let's take a simple task. We want to make a trigger for when the boss of P4S part 2 starts casting Heart Stake on you (and only you).
The log line would look something like this:
20|2022-03-29T19:58:58.1570000-07:00|40010363|Hesperos|6A2B|Heart Stake|108A7015|PLD Player|4.70|100.42|100.08|0.00|-2.81|4541079e7a27006f
We know that the 20
in the first field indicates an ability cast start, which is what we're looking for. 6A2B in the 5th field is the ability ID, which is better to use than the name because you don't have to worry about supporting other languages. Finally, the 7th and 8th fields give us the target ID and name. We don't really care about the rest of the fields.
So, one might be tempted to make a regex that looks something like this:
^20\|[^|]+\|[^|]+\|[^|]+\|6A2B\|[^|]+\|[^|]+\|(?<targetName>[^|]+)\|
However, we're just capturing the target name, we want the trigger to only fire if the target is the player. One option is to tell the user to fill in their own name. This is annoying for the end user, even more annoying if they have alts. It also falls apart for other things, like ignoring casts from fake actors, as such things rely on entity IDs which are not stable. The much more reasonable option is to use something like Triggernometry that lets us add additional conditions to the trigger, like this:
^20\|[^|]+\|[^|]+\|[^|]+\|6A2B\|[^|]+\|[^|]+\|(?<targetName>[^|]+)\|
where targetName = ${playerName}
Seems reasonable. However, the readability is poor. The [^|]+
is really just an obtuse way of saying "we do not care what is in this field". In addition, the actual conditions are split into two places - the '20' and '6A2B' are in the regex, while the 'target is me' condition is external to the regex.
So why don't we clean this up a bit:
^(?<lineType>[^|]+)\|[^|]+\|[^|]+\|[^|]+\|(?<abilityID>[^|]+)\|[^|]+\|[^|]+\|(?<targetName>[^|]+)\|
where lineType = 20
and abilityID = 6A2B
and targetName = ${playerName}
This is a bit more readable, since the important information is all pulled into capture groups with human readable names, and filtered through human readable conditions.
But wait, if we're no longer using the regex for conditions, but merely parsing, and every single 20-line has the same format, why are we even asking the user for a regex to begin with? Why not just give the user a list of pre-canned regices, with all the fields captured? There are only so many ACT events, right?
Right! The Cactbot log guide has exactly that - a collection of pre-canned regices for all of the relevant event types. Cactbot itself almost does that - it still builds a regex internally with the conditions built in, but you don't manually write it. Instead, it provides some helpers for building them out of human readable parameters:
{
id: 'P4S Heart Stake',
type: 'StartsUsing',
netRegex: NetRegexes.startsUsing({ id: '6A2B', source: 'Hesperos' }),
// ...
condition: Conditions.caresAboutPhysical(),
response: Responses.tankBuster(),
},
Don't get me wrong - that's still a hell of a lot better than writing a regex manually. It also takes the very important additional step of not requiring the user to actually know the log line format at all!
But...why not take it a step further? Cactbot has a mapping of all line types and their fields. It uses regex as little more than an implementation detail. So why not just use a simple string split on the |
character, map it up to fields, and then return a rich object with said fields?
Well, that's exactly what Triggevent does. It can do this for both triggers and overlays written in the codebase, as well as "Easy Triggers". The equivalent "Heart Stake" trigger looks like this, and even incorporates the NPC ID (which is stable, unlike the entity ID) to avoid any issues with fake/dummy actors:
I didn't even have to make that from scratch - I simply found a log where Heart Stake was used, right clicked on the event, and clicked "Make Easy Trigger". Then, I changed the condition from "Any Player" to "The Player" to make it only trigger when it is being cast on myself.
Oh, and the on-screen text? It even lists the duration remaining on the cast bar by default:
In addition, by using rich objects, we get to factor our own logic into them. For example, by keeping track of what status effects are on an entity, we can determine whether a buff application is an initial application or a refresh, despite there being no readily apparent difference in log lines. For example, the Death's Toll trigger uses this, since it is technically a "refresh" when a stack is consumed by getting hit. We only want to trigger on the initial buff application, hence the !buff.isRefresh()
condition:
@HandleEvents
public void deathsToll(EventContext context, BuffApplied buff) {
// Stack counting down would be considered a refresh
if (buff.getBuff().getId() == 0xACA) {
isDeathsToll = true; // Used to flip the callouts for placement of the Fledgling Flight markers
if (buff.getTarget().isThePlayer() && !buff.isRefresh()) {
long stacks = buff.getStacks();
ModifiableCallout<BuffApplied> callout = switch ((int) stacks) {
case 1 -> deathsToll1;
case 2 -> deathsToll2;
case 4 -> deathsToll4;
default -> deathsTollN;
};
context.accept(callout.getModified(buff));
}
}
}
This has several other benefits as well. For example, because you are given objects representing the source and the target of an ability, you can query things like the NPC ID that would normally come from a completely different line, because TE tracks such data internally and incorporates it into the data passed to the trigger. Same with player jobs and such. Furthermore, you avoid several pitfalls, like hex/dec conversion or worrying about leading zeroes on numbers.
In addition, it also means that if the log format changes, it can be updated in one place, rather than every trigger needing to incorporate changes. Because important bits of data can change at the whim of either Ravahn or SE, it's important to be able to update log parsing code quickly. Whereas, with something like Triggernometry, someone may have to painstakingly fix every regex in their triggers.
Lastly, it provides one more striking advantage - by having a layer of separation between triggers and the ACT log line format, by way of these event objects, you can test certain types of triggers (such as ability casts and buffs) using an FFLogs report.
Remember - the log line merely represents something that happened in game. It is no more than an implementation detail of the FFXIV ACT plugin.