Skip to content

Trigger State Updater (TSU)

Buds edited this page Aug 14, 2024 · 79 revisions

Introduction

At its core the Trigger State Updater custom trigger type (referred to as "TSU") provides a way to create "clones". That is many separate regions with their own separate Dynamic Info from a single trigger. If you're familiar with event type triggers or aura triggers that provide the "auto-clone" option then you'll understand the concept. The clones will all take their general settings from the Aura's main settings so positioning them will usually require a Dynamic Group or some potentially intricate coding. Likewise all other settings will be shared across all clones; the colour set in the Display tab, the text settings, sizing and so on, unless you specifically use Conditions or Animations to alter them. All TSU triggers are effectively "Status" type triggers, and will receive the dummy events just as normal Status triggers do.

Video Tutorials

A few video tutorials have been made over the last few expansions. While they don't always include features that have been added since then, everything in them is still relevant and useful info.

Pre-Legion:

Late-Legion:

BFA:

The "allstates" table

So how do you make a new State/Clone? When you make a new trigger with the Trigger State Updater type, WeakAuras will send the allstates table into that function as the first arg (followed by the event name and the event's own args, as normal). You make a new State by adding a subTable to allstates containing some specific settings, then returning true from the function.

A good way to conceptualise TSU is to think of the allstates table as the way that you can communicate with the WA addon and tell it what clones you want it to make, change, or remove. And to think of a separation between "states" and "clones". The WA addon reads the table of "states" that you give it and creates "clones" based on them. But, for instance, you can't simply change a "state" you previously made to nil and expect the associated "clone" to be removed. You need to change the state.show = false and let WA remove the clone. WA will also then remove the state table for you at that point. (Also, see below for relevant info on the importance of using changed)

States and Settings

When creating your state subTable in allstates the following settings can be used:

  • changed = boolean: Informs WeakAuras that the states values have changed. Always set this to true for states that were changed.
  • show = boolean: Controls whether the display is visible. Note, that states that have show set to false are automatically removed.
  • name = string: The name, returned by %n
  • icon = number or string: IconID or TexturePath, used in icons and progress bars
  • texture = number or string: IconID or TexturePath, used in textures
  • stacks = number: The stack count, returned by %s
  • index = number or string: Sets the order the output will display in a dynamic group (if sorting is set to "none" on the group). Strings or numbers are fine but DO NOT MIX TYPES!

Progress information

can be set by either setting progressType to "static" or "timed":

  • progressType = "timed"
    • expirationTime = number: relative to GetTime()
    • duration = number: total duration of the bar in seconds
      • For example, a simple 5 second timer would use duration = 5 and expirationTime = GetTime() + 5
  • progressType = "static"
    • value = number
    • total = number

Other "progress" related fields:

  • autoHide = boolean: Set to true to make the display automatically hide at the end of the "timed" progress. autoHide can also be used along with the "static" progressType by defining a duration and expirationTime along with the static value and total. While the static values will be displayed, the timed values will set the Hide time for the clone.
  • paused = boolean: Set to true (and set a remaining value) to pause a "timed" progress. Set to false (and recalculate the expirationTime value) to resume.
  • remaining = number: Only used with paused, gives WA the info needed to show paused progress at the current point.

Overlays

can be added to TSU bars.

  • additionalProgress = table: This is a more complex field than the others but allows you to create "Overlays" on bars that you make using TSU.
    • The additionalProgress table can have as many index-keyed subTables as you like. Each of those subTables contains the positional info for that overlay.
      • min = number
      • max = number
        OR
      • direction = string
      • width = number`
      • offset = number`

min and max go together as a pair. Their values should be relative to the main bar's total/duration. They define the left and right point of the overlay on the bar.

direction and width go together as a pair and define the position of the Overlay relative to the moving edge of the bar. direction should be either "forward" or "backward", dictating whether the Overlay will go ahead or behind the moving edge. width is a number relative to the total/duration.
offset is optional but can only be used with direction and width and is a simple offset from the bar's moving edge.

See the example below for how these would be used in an actual trigger

Tooltips

TSU triggers have the "Tooltip on Mouseover" option available by default in the Display tab. However you need to provide specific information in the state in order for the tooltip you want to show. With that option ticked you need to use:

  • spellId = number: To show a spell's tooltip
  • itemId = number : To show an item's tooltip
  • link = string : To show the tooltip of anything that can be put in a link. Useful, of course, for showing specific items (rather than generic ones from itemId) but also can show spells, achievements, quests, etc.
  • unit = string : a valid unitID
    • unitAuraInstanceID = number and unitAuraFilter = string : To show the tooltip for a specific aura on the specified unit.
    • unitBuffIndex = number : To show the tooltip for a specific buff on the specified unit.
      • unitBuffFilter = string : (Optional) To filter the buff in accordance with UnitBuff
    • unitDebuffIndex = number : To show the tooltip for a specific debuff on the specified unit.
      • unitDebuffFilter = string : (Optional) To filter the debuff in accordance with UnitDebuff
    • unitAuraIndex = number : To show the tooltip for a specific aura on the specified unit.
      • unitAuraFilter = string : (Optional) To filter the aura in accordance with UnitAura
  • tooltip = string : A string to be displayed. Escape sequence formatting can be used as needed.
  • tooltipWrap = bool : true to make the text on the tooltip wrap to new lines, false or nil to let it make the tooltip as wide as needed to fite the text given.

A spellId or itemId is enough to show the tooltip for those spells/items. To show a buff/debuff both the unit and the index must be given. The buff/debuff "index" is not the spellID of the buff but the index!

Units and Anchoring

  • unit = string: A valid UnitID.

WA has a few ways in which clone states can be directly associated with units in the UI, their Nameplates and UnitFrames. These can be found in the Display tab's Anchor To setting, a Dynamic Group's "Group By" setting, as well as options for setting External Glows in Conditions and Actions. To enable these settings your state must carry a valid UnitID.

Custom fields

The only other thing to mention in the state settings is that you can add your own fields to the state. These won't be processed by WeakAuras or have any effect in themselves but it attaches some info to that state that you can use in various ways.

  • customField = value

Example Trigger Function

This simple example aims to make a cast bar for any group members that cast a specific spell. It would use custom - event - COMBAT_LOG_EVENT_UNFILTERED:SPELL_CAST_START for the trigger settings and the following in the trigger:

function(allstates, event, _, subEvent, _, _, sourceName, _, _, _, _, _, _, spellID)
    if subEvent == "SPELL_CAST_START"
    and UnitExists(sourceName) 
    and spellID == 123456 
    -- Is the combat log event one we care about?
    -- UnitExists(sourceName) is a quick way to see if the caster is in our group 
    --     since unit names are also unitIDs for group members.
    then
        local name, _, icon, startMS, endMS = UnitCastingInfo(sourceName)
        -- We gather the info we need
        if name then -- quick nil check
            -- calculate the duration info we'll need
            local duration = (endMS - startMS) / 1000
            local expiration = endMS / 1000
            -- and this is the TSU bit! 
            -- We're making a new subTable in `allstates` using the sourceName as the table key.
            allstates[sourceName] = {
                show = true,
                changed = true,
                progressType = "timed",
                duration = duration,
                expirationTime = expiration,
                name = name,
                icon = icon,
                caster = sourceName,
                autoHide = true,
                additionalProgress = {
                    {
                        min = 0,
                        max = 0.5,
                    },
                    {
                        direction = "forward",
                        width = 2,
                        offset = duration/3
                    },
                }
            }
            return true
            -- and once we return true, WA will process the additions to `allstates` and create the new clone. 
        end
    end
end

return, show, changed

The returned value from a TSU trigger does not affect the Activation state of the trigger, the trigger will be activated as long as any of its states are set to show = true. So all the return value does is tell WeakAuras to check the states for updates (changed = true). As such, common mistakes are setting updates to the states' info but not returning true to tell WeakAuras to show that updated info, or making changes, returning true, but not setting changed = true on the states that need updating.

Custom Variables

A code block is available below the trigger function on a TSU trigger and this allows us to define State fields for use in the Conditions tab. It is also used to specify whether the trigger will be using any Overlays, enabling colour-pickers for the Overlays in the Display tab.

The code block doesn't expect a function like most code blocks in WA. All we're doing is defining a table, so the info you give it will be enclosed inside { } and the various settings we create are members of that table.

Overlays

additionalProgress = number : This number sets how many Colour-pickers should be added to the Display tab and should, of course, be set to the maximum number of "additionalProgress" tables you will be using in your trigger.

Conditions

Conditions can be set in slightly different ways depending on the complexity required.

Standard Conditions:

If you want to include "standard" conditions from your TSU clones then you can simple declare them to be true. All the default Conditions available to use in this way are:

{
    expirationTime = true,
    duration = true,
    value = true,
    total = true,
    stacks = true,
}

Custom Conditions:

To include your own custom state fields in the Conditions settings you need to tell WA the type of variable that field represents. The options are:

  • "bool" - Provides a dropdown with true or false
  • "string" - Provides a dropdown for "Is Exactly", "Contains", and "Matches (pattern)", along with a text box to enter a string to compare to.
  • "number" - Provides a dropdown with comparators (<, >=, etc.) along with a box to enter a numbers to compare to.
  • "timer" - Provides the same controls as "number" but compares against the remaining duration on that given timer. The value used for this kind of condition needs to be relative to the value of GetTime(). Equivalent to the "Remaining Duration" Condition found on timed non-custom Triggers.
  • "elapsedTimer" - As above but equivalent to "Since" Conditions sometimes found on non-custom Triggers, providing the ability to be active based on the number of seconds since your value.

Example:

{
    var1 = "bool",
    var2 = "number",
}

Complex Custom Conditions:

Instead of simply setting the type of variable you can use a sub-table. This table can be used to set a specific display name for the Condition, and access the "select" Condition type. The "select" type lets you define a series of optional values which will appear as a dropdown selection. These should be defined in a further sub-table with each table Key being the value set on the State and the table Value being what should be visible to users in Conditions.

var1 = {
    display = "my var name",
    type = "string"
}
var2 = {
    display = "var2",
    type = "select",
    values = {
        ["y"] = "yes", 
        ["n"] = "no", 
        ["m"] = "maybe"
    }
}

All the above methods allow you create a Condition that simply compares against a single value that exists on the State. Complex Custom Conditions can also allow the user provide a custom function to decide the Condition's state. This function, called test, is sent the State table of the State to be evaluated followed by the "needle" value(s) (that is value(s) chosen by users in the Conditions tab), and evaluates the resulting Condition's outcome using code.

A simple example:

isMoving = {
    display = "Player moving",
    type = "bool",
    test = function(state, needle)
        return IsPlayerMoving() == (needle == 1)
    end,
    events = {
        "PLAYER_STARTED_MOVING", "PLAYER_STOPPED_MOVING" 
    },
}

So you can see that we're defining a function in the table, which must be called test, that simply checks the API function IsPlayerMoving() and compares it to the "needle" provided by the selection made in the Conditions tab.

A note on the "needle": Remember that a "bool" type Condition is a dropdown selection so the values selected there provide 0 or 1, not false or true (or even "false" or "true") as might be intuitive. Also note that depending on the type of Condition being used there may be more than one needle value sent in. For example if you set a "number" type Condition then there will be two needle args sent in to the function, a value and a comparison (e.g. 3 and ">="). Be sure to catch all needle args.

We're also providing an events variable, an array containing any game events on which this Condition will be assessed. This is only necessary if your trigger is not handling all the events needed to update the Condition. All Conditions are evaluated when the trigger returns true so in this example case if our trigger were handling those "MOVING" events then specifying them wouldn't be necessary.

This is, of course, a very simple example but you have access to all the State table's values along with the whole API so the possibilities are almost endless.

I think it is worth mentioning however, that everything that can be done with Custom Tests can be done in the trigger and carried through to Conditions via a simple Custom Condition. If we take the "Player moving" example above, you could add the necessary events to your trigger, use them to iterate existing states and update a simple variable like state.moving = true which could then be defined for use in Conditions. The method used can come down to just what you find easiest or most intuitive.

Custom Variables Conclusion :

All these approaches to settings can be mixed and matched as you need.
A Full Example:

{
    additionalProgress = 1,
    
    expirationTime = true,
    stacks = true,
    
    unit = "string",
    
    var3 = {
        display = "colour",
        type = "select",
        values = {
            [1] = "red",
            [2] = "blue",
            [3] = "green",
        }
    },

    spellUsable = {
        display = "Spell Usable",
        type = "bool",
        test = function(state, needle)
            return state and state.show and (IsUsableSpell(state.spellname) == (needle == 1))
        end,
        events = {
            "SPELL_UPDATE_USABLE",
            "PLAYER_TARGET_CHANGED",
            "UNIT_POWER_FREQUENT",
        },
    },
}

State Table Keys

Controlling the table key you use when you create a state gives control over how States are altered, overwritten, or when more clones are made.
Overwriting an existing state table with new info will change that existing state. If you always used allstates["state"] as your only key then you would never make more clones, and will always overwrite the first. If you used allstates[#allstates+1] then you'd always make new clones. However if you know which clones you will want to show and when, then overwriting is actually useful.
Say, using the above example function, you wanted to track casts of 3 given spellIDs, but only ever wanted to show the most recent use of each of the spells no matter who cast it. You could use the spellID as the State key and the overwriting will handle itself.

"aura_env.state"

All the info that is put into a state in the trigger function is also passed into the various custom code functions around the Aura (e.g. Animations, Custom Text, On Show/Hide, etc.). You can access this info using the aura_env.state variable.

Animations example: If we had also added a field to our state called colour and we set that to be either "Red", "Green", or "Blue" then we could access that info in an animation function and use it to alter the colour of that specific clone.

function()
    if not aura_env.state then return 0, 0, 0, 1 end  -- error checking.
    if aura_env.state.colour == "Green" then
        return 0, 1, 0, 1 -- Reminder: return R, G, B, Alpha (using numbers between 0 and 1)
    elseif aura_env.state.colour == "Red" then
        return 1, 0, 0, 1
    elseif aura_env.state.colour == "Blue" then
        return 0, 0, 1, 1
    else
        return 1, 1, 1, 1
    end 
end

Each variable that you save in the state can be accessed like this and will run these scripts for each state. Imagine that instead of a simple string "Red" you instead sent a small table of RGB values. colour = {1,0.25,0}. Then your animation function can simply output the RGB values from that table. The possibilities that are opened up with custom fields are great.

State Fields and Text Output

Also worth noting that all field in the State can be outputted in text directly using %fieldName. In the full Trigger function example above the custom field "caster" is added to the State. Without needing any custom text function the value of that field, the caster's name, can be outputted, using %caster

A very important note about making changes to clone regions

In WoW's UI, frames once created, can not be destroyed and a UI reload is the only thing that will reset all the frames that have been made. As such, any well-made addon that creates frames programmatically will use some method (usually a "pool") to reuse old frames and prevent infinite creation of more and more. WeakAuras uses this method for its clone regions. When a trigger needs to make a new clone it first checks if any old ones are in the pool, available for reuse, before creating an additional one.

Now, with all of that in mind, how does this impact using TSUs?
Any change you make to a clone region that isn't automatically reset by WeakAuras when the clone returns to the clone pool will continue to affect other Auras that use pull from that pool. For example, if you're making a progress bar TSU and you want a central text you might be tempted to add an additional fronString frame (as is the recommendation when not using clones). However, WeakAuras does not know that this has been added to the clone region and so next time the region is used there will be an extra text in the middle, stuck showing whatever text it was last given.
Other common examples of similar mistakes are using SetScript to make the regions clickable, adding "tick" textures, and using SetPoint to re-anchor.

The temptation might be to just use On Hide to undo anything you did, however On Hide is not run in all circumstances, such as when opening WA config. The recommendation really is to just avoid making changes to the region that could "pollute" the pool. For anchoring the solution is to avoid SetPoint but instead use region:SetAnchor(point, frame, framePoint) and region:SetOffset(x, y).

TSU - Frequently asked questions and some frequently made mistakes

Q: How do I clear out all current clones?

To remove existing states you can't simply clear the allstates table. You need to tell WA to update those states, even just to clear them. The snippet below achieves that:

for _, state in pairs(allstates) do
    state.show = false;
    state.changed = true;
end

Q: If I use multiple TSU triggers in an Aura do they share an allstates table?

No. All TSU triggers have separate tables.
A good way to think of States is that they're the "Dynamic Info" (see this wiki page if you're not confident about that) of that specific trigger. So just as with normal triggers, if you change where the Dynamic Info is coming from, from one trigger to another and the Aura's output changes. With TSU triggers, however, instead of the Aura just updating to show different info, the trigger's clones are displayed.
If you feel like you want access to one trigger's allstates table from inside another trigger then you're probably handling events in different triggers and perhaps you haven't noticed that you can handle as many events as you want in the main trigger.

Clone this wiki locally