Writing your first addon

From ESOUI Wiki

Jump to: navigation, search

So you've decided that you're interested in creating UI addons for Elder Scrolls Online. Great! Here's a walk-through to get you started.

For this example addon, we'll implement a simple indicator of whether or not the player is in combat.

Contents

Addon file structure

If you're looking to write addons, you've probably already used some. The addons that you've installed are located in the AddOns subfolder in your user folder. Typically each addon will have its own named directory within the AddOns directory. This keeps all of the addon's files in one tidy collection.

Within an addon's folder, there are 4 types of files:

For the most basic of addons, you only need two files - the metadata file and a code file:

Elder Scrolls Online\live\AddOns
  |
  +-- FooAddon\
        |
        +-- FooAddon.txt
        |
        +-- FooAddon.lua

Manifest file (.txt)

It is customary to name both the directory and the metadata file using the addon's name to make them easy to find, so if your addon is named "Foo Addon", then the directory would be FooAddon and the metadata file would be FooAddon.txt to match.

## Title: Foo Addon
## APIVersion: 100017

FooAddon.lua

While there are many options you can specify in your addon's metadata file, the most important ones (and the ones that every addon should have set) are the Title and APIVersion. The title is what will be displayed in the in-game addon list, and the APIVersion needs to match the current APIVersion specified by the client or else the addon will be considered "out of date" and disabled by default (to prevent completely broken UIs when Zenimax updates the API).

After the options comes a list of other files that should be loaded for the addon. In this starter addon, all there will be is a Lua file, so that is the only file listed.

Code files (.lua)

The code for your addon is where you actually implement whatever functionality you want your addon to have. Each code file will be loaded and run when the UI is initially loaded. Your addon can have as many code files as you want - addon authors will often split up code between multiple files to help keep it organized.

Try it - create an empty addon.

Try creating the directory and the two files mentioned above. Fill in FooAddon.txt with the items mentioned above. Leave FooAddon.lua as an empty file for now. Once you've saved both files, if you /reloadui in game (or press E from the addons window), your new addon should show up in the list. It won't do much of anything yet though since it has no code.

Basic skeleton code

We'll start off by creating a small amount of code (in FooAddon.lua) designed to give our addon a little structure and help it load gracefully. Note that all of the lines beginning with -- below are comments - they don't affect the actual code.

-- First, we create a namespace for our addon by declaring a top-level table that will hold everything else.
FooAddon = {}
 
-- This isn't strictly necessary, but we'll use this string later when registering events.
-- Better to define it in a single place rather than retyping the same string.
FooAddon.name = "FooAddon"
 
-- Next we create a function that will initialize our addon
function FooAddon:Initialize()
  -- ...but we don't have anything to initialize yet. We'll come back to this.
end
 
-- Then we create an event handler function which will be called when the "addon loaded" event
-- occurs. We'll use this to initialize our addon after all of its resources are fully loaded.
function FooAddon.OnAddOnLoaded(event, addonName)
  -- The event fires each time *any* addon loads - but we only care about when our own addon loads.
  if addonName == FooAddon.name then
    FooAddon:Initialize()
  end
end
 
-- Finally, we'll register our event handler function to be called when the proper event occurs.
EVENT_MANAGER:RegisterForEvent(FooAddon.name, EVENT_ADD_ON_LOADED, FooAddon.OnAddOnLoaded)

This skeleton code does 3 main things:

Save FooAddon.lua with the new code. If you reload the UI, it still won't visibly do anything since we haven't yet added the actual functionality - but you shouldn't get any UI errors either.

Adding functionality

Now it's time to make our addon actually do something visible. We want to create an indicator for whether or not the player is in combat. We'll start out by just displaying a message whenever the player enters and exits combat. To do this, we're going to need to handle another event.

If you look at the list of events provided by the API, you'll find EVENT_PLAYER_COMBAT_STATE among them. This event occurs whenever the player's combat state changes. It passes along a flag which indicates the player's new combat state - true if the player is in combat, and false if the player is not in combat. We'll create a handler for this event.

For now, we'll use a function that's part of the API, d(), to output a message to the chat window. d is short for "debug" - it's intended to mostly be used by developers like you for outputting information during the development process.

Getting the initial combat state

First we'll want to add a variable to our addon's table to keep track of the player's current combat state, and initialize it when our addon loads. Change the FooAddon:Initialize function to look like this:

function FooAddon:Initialize()
  self.inCombat = IsUnitInCombat("player")
end

In this function, self is automatically set to whatever :Initialize() was called on - in this case, since we call the function from OnAddOnLoaded as FooAddon:Initialize(), self is just a shorter way of referring to FooAddon. This is automatic when using : to define and call functions rather than . - it's how "methods" can be implemented in Lua. You're not required to use : syntax - you could just use . and type out FooAddon.inCombat instead - but you may see this syntax in other code that you look at for examples, so it's good to know what it means.

In this case, the new code creates a variable in the FooAddon table called inCombat, and initializes it to the current combat state of the player via a call to the API function IsUnitInCombat().

Handling combat state changes

Now that we have the current state, we want to detect when it changes. We'll create and register another event handler to do this.

First, let's create the event handler that will be called when combat state changes. Add it as a new function:

function FooAddon.OnPlayerCombatState(event, inCombat)
  -- The ~= operator is "not equal to" in Lua.
  if inCombat ~= FooAddon.inCombat then
    -- The player's state has changed. Update the stored state...
    FooAddon.inCombat = inCombat
 
    -- ...and then announce the change.
    if inCombat then
      d("Entering combat.")
    else
      d("Exiting combat.")
    end
 
  end
end

Once we've created the function, we need to register it as a handler for the event. We'll do this from the Initialize function (unlike the addon load event, which we did from the top level of the file), because we only want to start handling most events after our addon has completely loaded.

Update our initialize function so it looks like this:

function FooAddon:Initialize()
  self.inCombat = IsUnitInCombat("player")
 
  EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_COMBAT_STATE, self.OnPlayerCombatState)
end

Now if you save the file and then /reloadui in game, you should see messages appear in the first chat tab whenever your character enters and exits combat.

Adding a graphical component

A message in the chat window proves our addon is actually doing something, but it'd be nice to have something that's part of a more visual UI. Let's change it to a big red "Fighting!" notice that appears on-screen whenever we're in combat.

There are a couple of different ways to create controls (graphical units) in ESO. For this particular tutorial, we'll do it using XML definitions, but it's also possible to create controls through pure Lua code by making use of existing templates already defined by the game.

Creating a graphical control in XML

First, create FooAddon.xml - this is where our control definitions will go. Then update FooAddon.txt to list FooAddon.xml right before FooAddon.lua. (Files are loaded in the order listed, so we want to make sure our controls are loaded before our code might use them.)

Next, add the following content to FooAddon.xml and save the file:

<GuiXml>
  <Controls>
    <TopLevelControl name="FooAddonIndicator">
      <Dimensions x="200" y="25" />
      <Anchor point="BOTTOM" relativeTo="GuiRoot" relativePoint="CENTER" offsetY="-20" />
 
      <Controls>
        <Label name="$(parent)Label" width="200" height="25" font="ZoFontWinH1" inheritAlpha="true" color="FF0000"
            wrapMode="TRUNCATE" verticalAlignment="TOP" horizontalAlignment="CENTER" text="Fighting!">
          <Anchor point="TOP" relativeTo="$(parent)" relativePoint="TOP" />
        </Label>
      </Controls>
    </TopLevelControl>
  </Controls>
</GuiXml>

You're probably wondering what exactly all of this does. Here's an explanation of each part:

GuiXml
This is the wrapper tag for any XML file used for constructing ESO UI elements.
Controls
This container holds any definitions of controls.
TopLevelControl
Any control which isn't a child of another control is a TopLevelControl. Typically this means the highest level of organization for a given UI element - for instance the collection of all chat windows might all be in one single TopLevelControl for the chat system.
Dimensions
This tag is used to specify the size of a control. (It's often possible to do this via width and height attributes instead.)
Anchor
Anchors define how various controls should be positioned relative to one another.
This anchor places our overall control group a bit above the center of the screen.
Controls
Once again, a container for controls - though in this case, the children of the current TopLevelControl, rather than a collection of top-level controls.
Label
This defines a label - a piece of text that will be rendered as part of a UI element. It has various attributes used to customize how the text is displayed.
Anchor
This anchor makes the label line up with its parent control. Since they're the same size, any two matching points would do.

The $(parent) placeholder is filled in by ESO when it loads the template - it automatically fills in the name of the control's parent. This can be especially useful when creating reusable templates for controls that might have different names. In this case, its usage means that the Label will wind up having the name FooAddonIndicatorLabel.

Manipulating controls in Lua

Now that we have a defined control, we can use Lua to manipulate it. Go back to FooAddon.lua and change FooAddon.OnPlayerCombatState to look like this:

function FooAddon.OnPlayerCombatState(event, inCombat)
  -- The ~= operator is "not equal to" in Lua.
  if inCombat ~= FooAddon.inCombat then
    -- The player's state has changed. Update the stored state...
    FooAddon.inCombat = inCombat
 
    -- ...and then update the control.
    FooAddonIndicator:SetHidden(not inCombat)
  end
end

In this new version of the function, we're calling the :SetHidden() method of the control to change whether or not the "Fighting!" text is displayed. We use not to invert the value of inCombat, since we want the text to be visible when the player is in combat, and hidden when not in combat.

Make sure you've saved all of the files, then try reloading the UI. You should see the large red text appear when you go into combat and disappear when you leave combat. (Remember that it can often take some time to leave combat after you've stopped fighting.)

Saving settings

My that big red text is kind of imposing, isn't it? Especially when it's smack-dab center in the screen. Let's make it so that the user can move the text somewhere else and it'll stay where they've moved it.

In order to do this we're going to have to be able to save certain values between UI reloads (and between game starts). We do this using what's called SavedVariables.

Adding a SavedVariable

First we need to tell ESO that our addon has something to save. Update the contents of FooAddon.txt to look like this:

## Title: Foo Addon
## APIVersion: 100009
## SavedVariables: FooAddonSavedVariables

FooAddon.xml
FooAddon.lua

Adding the SavedVariables line lets ESO know that it should save the data referenced by FooAddonSavedVariables whenever the UI is unloaded.

Next we need to actually set up the SavedVariables object in our Lua code. While it's possible to just read and write directly to the name specified as the SavedVariables entry (it behaves like a Lua global variable), there is also a built-in module that provides management of per-character settings, versioning, and other features related to saving data. We'll use it here to show you how. Update the FooAddon:Initialize() function to look like this:

function FooAddon:Initialize()
  self.inCombat = IsUnitInCombat("player")
 
  EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_COMBAT_STATE, self.OnPlayerCombatState)
 
  self.savedVariables = ZO_SavedVars:New("FooAddonSavedVariables", 1, nil, {})
end

This creates a ZO_SavedVars object which wraps the raw SavedVariables entry and will automatically load a unique set of settings for each character. If you want your addon's settings to be account-wide instead of per-character, you can use ZO_SavedVars:NewAccountWide() instead.

Making the indicator movable

In order to allow the user to change where the indicator is, it's going to need to be movable. To do that, update the TopLevelControl in FooAddon.xml to look like this:

    <TopLevelControl name="FooAddonIndicator" mouseEnabled="true" movable="true" clampedToScreen="true">

The new attributes added for the TopLevelControl tell ESO to make it so that the user can drag the control around, as long as it stays somewhere on the screen. If you save everything and /reloadui, you should notice that you're able to drag the text around now (when it's visible, that is - for testing, you might modify the code to make it always visible). If you /reloadui again, however, the position will reset, because we're not actually saving it yet.

Saving the indicator's position

In order to remember where the user placed the indicator we're going to have to store its position in our SavedVariable and then load that position whenever our addon loads. Let's tackle the saving portion first. In order to save the position, we'll need to know when it changes. Update FooAddon.xml to look like this:

<GuiXml>
  <Controls>
    <TopLevelControl name="FooAddonIndicator" mouseEnabled="true" movable="true" clampedToScreen="true">
      <Dimensions x="200" y="25" />
      <Anchor point="BOTTOM" relativeTo="GuiRoot" relativePoint="CENTER" offsetY="-20" />
 
      <OnMoveStop>
        FooAddon.OnIndicatorMoveStop()
      </OnMoveStop>
 
      <Controls>
        <Label name="$(parent)Label" width="200" height="25" font="ZoFontWinH1" inheritAlpha="true" color="FF0000"
            wrapMode="TRUNCATE" verticalAlignment="TOP" horizontalAlignment="CENTER" text="Fighting!">
          <Anchor point="TOP" relativeTo="$(parent)" relativePoint="TOP" />
        </Label>
      </Controls>
    </TopLevelControl>
  </Controls>
</GuiXml>

The OnMoveStop block we've added will call another event handler on our object whenever the user finishes dragging our indicator around. Now we just need to add that handler to FooAddon.lua so it can be called:

function FooAddon.OnIndicatorMoveStop()
  FooAddon.savedVariables.left = FooAddonIndicator:GetLeft()
  FooAddon.savedVariables.top = FooAddonIndicator:GetTop()
end

This function is pretty straightforward - it just stores the position of the top-left corner of our control in our saved variables table. Of course, just saving the position isn't much use if we don't load it, so let's add a function to restore the position and update our initialization function one more time to call it:

function FooAddon:RestorePosition()
  local left = self.savedVariables.left
  local top = self.savedVariables.top
 
  FooAddonIndicator:ClearAnchors()
  FooAddonIndicator:SetAnchor(TOPLEFT, GuiRoot, TOPLEFT, left, top)
end
 
function FooAddon:Initialize()
  self.inCombat = IsUnitInCombat("player")
 
  EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_COMBAT_STATE, self.OnPlayerCombatState)
 
  self.savedVariables = ZO_SavedVars:New("FooAddonSavedVariables", 1, nil, {})
 
  self:RestorePosition()
end

There's a couple of new things in this added code. First, note that we're using local variables inside a function. Most variables that are only relevant to a particular function should be made local to avoid polluting the global namespace which is shared by all addons and functions. (This is also why we've put all of our addon's functions inside a top-level table - that way, only a single table name is global, which is unlikely to cause any conflicts since it's named after our addon.)

Second, we've introduced a few more API functions, specifically ones that operate on controls. :ClearAnchors() tells ESO to remove any existing layout anchors defining how a given control should be positioned. We do this because we then add a new anchor which positions the control based on our saved position. :SetAnchor() is what does that adding, and in this case we're telling the game to align the control's top-left corner relative to the top-left corner of the GuiRoot, which is how ESO refers to the entire screen. The last two arguments are the offset between the two points of the anchor, which allows us to specify the exact position.

Save everything, /reloadui, and you should now have an indicator that can be dragged around the screen and will stay wherever it's dragged, even across UI reloads.

Behaviour on /reloadui

Since you're doing a lot of /reloadui, you've likely noticed that the indicator is visible after each one, even if you are not in combat. The simplest way to handle this is to add hidden="true" to the TopLevelControl:

<TopLevelControl name="FooAddonIndicator" mouseEnabled="true" movable="true" clampedToScreen="true" hidden="true">

If you do this, and then are a bit of a daredevil and /reloadui in combat, you'll notice something odd: The indicator isn't visible! While it's rare for players to reloadui under combat conditions, it is still a bug, and here's two ways you can deal with it:

1. Check if the player is in combat in the Initialized() function, and set it visible/hidden as needed. Adding the line FooAddonIndicator:SetHidden(not IsUnitInCombat('player')) to your initialized() function will set it appropriately. IsUnitInCombat takes in one argument, a unitTag, and returns true if the unit is in combat, or false if they are not. You can find more information on unitTags here: http://wiki.esoui.com/UnitTag 2. Use the <OnInitialized> field in the XML:

<GuiXml>
  <Controls>
    <TopLevelControl name="FooAddonIndicator" mouseEnabled="true" movable="true" clampedToScreen="true">
      <Dimensions x="200" y="25" />
      <Anchor point="BOTTOM" relativeTo="GuiRoot" relativePoint="CENTER" offsetY="-20" />
      <OnInitialized>
          -- LUA code here. This is where you'd check to see if the control needs to be hidden or not.
          FooAddonIndicator:SetHidden(not IsUnitInCombat('player'))
      </OnInitialized>
      <OnMoveStop>
        FooAddon.OnIndicatorMoveStop()
      </OnMoveStop>
 
      <Controls>
        <Label name="$(parent)Label" width="200" height="25" font="ZoFontWinH1" inheritAlpha="true" color="FF0000"
            wrapMode="TRUNCATE" verticalAlignment="TOP" horizontalAlignment="CENTER" text="Fighting!">
          <Anchor point="TOP" relativeTo="$(parent)" relativePoint="TOP" />
        </Label>
      </Controls>
    </TopLevelControl>
  </Controls>
</GuiXml>

Further reading

You now have a basic addon which encompasses many of the various techniques used by addon developers to create all manner of addons. If you're looking for more information to help you along the way, here are a few recommendations.

Personal tools
Namespaces
Variants
Actions
Menu
Wiki
Toolbox